@olares/lares-core 0.15.0
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 +23 -0
- package/brand/identity.d.ts +9 -0
- package/brand/identity.js +82 -0
- package/brand/manifest.js +15 -0
- package/drive/download.js +40 -0
- package/drive/execute.js +143 -0
- package/drive/files-path.js +55 -0
- package/drive/ls.js +126 -0
- package/drive/paths.js +320 -0
- package/drive/tools.js +206 -0
- package/drive/url-download.js +314 -0
- package/files/deliverables.js +32 -0
- package/files/disposition.js +20 -0
- package/files/filename.js +22 -0
- package/files/intake.js +217 -0
- package/files/limits.js +1 -0
- package/files/markdown.js +88 -0
- package/files/mention.js +64 -0
- package/files/preview-cache.js +100 -0
- package/files/preview-groups.js +21 -0
- package/files/preview-workspace.js +342 -0
- package/files/preview.js +305 -0
- package/files/upload-client.js +40 -0
- package/files/upload-http.js +22 -0
- package/files/upload.js +104 -0
- package/i18n/chat-model-switch.js +41 -0
- package/i18n/chat-model.js +40 -0
- package/i18n/files.js +39 -0
- package/i18n/mobile.js +115 -0
- package/i18n/preview.js +57 -0
- package/i18n/search.js +33 -0
- package/i18n/t.js +22 -0
- package/i18n/voice.js +70 -0
- package/icons/mark.js +22 -0
- package/larepass/chat.js +134 -0
- package/larepass/context-provenance.js +65 -0
- package/larepass/host.js +80 -0
- package/larepass/mux.js +194 -0
- package/larepass/questions.js +16 -0
- package/larepass/rpc.js +60 -0
- package/larepass/runtime.js +395 -0
- package/larepass/session-cache.js +104 -0
- package/larepass/settings.js +100 -0
- package/larepass/stage-copy.js +16 -0
- package/larepass/tool-row.js +132 -0
- package/larepass/transcript.js +332 -0
- package/media/ffmpeg-run.js +117 -0
- package/media/router-images.js +63 -0
- package/olares/entrance.js +45 -0
- package/olares/identity.js +190 -0
- package/olares/session-identity.js +48 -0
- package/olares/trusted-host.js +37 -0
- package/package.json +111 -0
- package/router/catalog-sync.js +87 -0
- package/router/catalog.d.ts +15 -0
- package/router/catalog.js +73 -0
- package/router/gateway.d.ts +4 -0
- package/router/gateway.js +21 -0
- package/router/models-client.js +39 -0
- package/router/models.d.ts +16 -0
- package/router/models.js +133 -0
- package/router/search.js +142 -0
- package/router/session-model.js +143 -0
- package/router/shim.js +134 -0
- package/router/stt.js +200 -0
- package/search/client.js +38 -0
- package/search/config.js +98 -0
- package/search/menu.js +22 -0
- package/tools/async.js +34 -0
- package/tools/crypto-uuid.js +26 -0
- package/tools/http.js +123 -0
- package/tools/json-file.js +18 -0
- package/voice/client.js +90 -0
- package/voice/config.js +59 -0
- package/voice/languages.js +29 -0
- package/voice/recorder.js +43 -0
- package/voice/service.js +83 -0
- package/workspace/env.js +13 -0
- package/workspace/home.js +9 -0
- package/workspace/path.js +80 -0
- package/workspace/seed.js +13 -0
- package/workspace/session.js +42 -0
package/i18n/mobile.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export const ZH = {
|
|
2
|
+
"shell.title": "Lares",
|
|
3
|
+
"shell.subtitle": "LarePass 内的对话入口",
|
|
4
|
+
"probe.idle": "尚未探测 Host",
|
|
5
|
+
"probe.loading": "正在连接 Lares Host…",
|
|
6
|
+
"probe.ok": "已连上 Host",
|
|
7
|
+
"probe.unauthorized": "Host 拒绝了当前登录态({http})",
|
|
8
|
+
"probe.missing": "还没有 Lares 入口地址",
|
|
9
|
+
"probe.error": "Host 返回 {http}",
|
|
10
|
+
"probe.unreachable": "连不上 Host:{error}",
|
|
11
|
+
"probe.retry": "重新探测",
|
|
12
|
+
"probe.entrance": "入口 {origin}",
|
|
13
|
+
"chat.empty": "发一条消息开始对话",
|
|
14
|
+
"chat.placeholder": "发消息或按住说话",
|
|
15
|
+
"chat.send": "发送",
|
|
16
|
+
"chat.sending": "正在回复…",
|
|
17
|
+
"action.copy": "复制",
|
|
18
|
+
"action.copied": "已复制",
|
|
19
|
+
"action.like": "点赞",
|
|
20
|
+
"action.dislike": "点踩",
|
|
21
|
+
"action.share": "转发",
|
|
22
|
+
"action.bar": "回复操作",
|
|
23
|
+
"composer.attach": "添加文件",
|
|
24
|
+
"composer.voice": "语音输入",
|
|
25
|
+
"composer.think": "深度思考",
|
|
26
|
+
"composer.search": "智能搜索",
|
|
27
|
+
"composer.hold": "松开发送",
|
|
28
|
+
"composer.searchFailed": "无法切换智能搜索",
|
|
29
|
+
"chat.failed": "发送失败:{error}",
|
|
30
|
+
"chat.openFailed": "打不开会话:{error}",
|
|
31
|
+
"chat.unauthorized": "当前登录态无法访问 Host",
|
|
32
|
+
"message.contextInjection": "Context injection",
|
|
33
|
+
"message.contextRecall": "Session recall",
|
|
34
|
+
"message.retry.active": "Retrying model request",
|
|
35
|
+
"message.retry.cancelled": "Model request retry cancelled",
|
|
36
|
+
"message.retry.started": "Retried model request",
|
|
37
|
+
"message.retry.scheduled": "Waiting to retry model request",
|
|
38
|
+
"message.retry.status": "{label} ({retry}/{maximum}) · {seconds}s",
|
|
39
|
+
"message.retry.delay": "Retry delay: ",
|
|
40
|
+
"message.retry.failure": "Failure reason: ",
|
|
41
|
+
"bar.history": "历史",
|
|
42
|
+
"bar.menu": "菜单",
|
|
43
|
+
"bar.new": "新对话",
|
|
44
|
+
"bar.newAria": "新对话",
|
|
45
|
+
"history.empty": "还没有其他对话",
|
|
46
|
+
"history.untitled": "新对话",
|
|
47
|
+
"history.loading": "正在加载对话列表…",
|
|
48
|
+
"chat.loading": "正在加载对话…",
|
|
49
|
+
"history.today": "今天",
|
|
50
|
+
"history.week": "之前 7 天",
|
|
51
|
+
"history.older": "更早",
|
|
52
|
+
"agent.refresh": "刷新",
|
|
53
|
+
"agent.loading": "正在读取配置…",
|
|
54
|
+
"question.title": "需要你选一项",
|
|
55
|
+
"question.waiting": "正在等待 Host 提问通道…",
|
|
56
|
+
"question.busy": "正在提交…",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const EN = {
|
|
60
|
+
"shell.title": "Lares",
|
|
61
|
+
"shell.subtitle": "Chat inside LarePass",
|
|
62
|
+
"probe.idle": "Host not probed yet",
|
|
63
|
+
"probe.loading": "Connecting to the Lares Host…",
|
|
64
|
+
"probe.ok": "Host reachable",
|
|
65
|
+
"probe.unauthorized": "Host rejected this login ({http})",
|
|
66
|
+
"probe.missing": "No Lares entrance URL yet",
|
|
67
|
+
"probe.error": "Host returned {http}",
|
|
68
|
+
"probe.unreachable": "Host unreachable: {error}",
|
|
69
|
+
"probe.retry": "Probe again",
|
|
70
|
+
"probe.entrance": "Entrance {origin}",
|
|
71
|
+
"chat.empty": "Send a message to start chatting",
|
|
72
|
+
"chat.placeholder": "Message or hold to speak",
|
|
73
|
+
"chat.send": "Send",
|
|
74
|
+
"chat.sending": "Replying…",
|
|
75
|
+
"action.copy": "Copy",
|
|
76
|
+
"action.copied": "Copied",
|
|
77
|
+
"action.like": "Like",
|
|
78
|
+
"action.dislike": "Dislike",
|
|
79
|
+
"action.share": "Share",
|
|
80
|
+
"action.bar": "Reply actions",
|
|
81
|
+
"composer.attach": "Add file",
|
|
82
|
+
"composer.voice": "Voice input",
|
|
83
|
+
"composer.think": "Deep think",
|
|
84
|
+
"composer.search": "Search",
|
|
85
|
+
"composer.hold": "Release to send",
|
|
86
|
+
"composer.searchFailed": "Could not toggle search",
|
|
87
|
+
"chat.failed": "Send failed: {error}",
|
|
88
|
+
"chat.openFailed": "Could not open a session: {error}",
|
|
89
|
+
"chat.unauthorized": "This login cannot reach the Host",
|
|
90
|
+
"message.contextInjection": "Context injection",
|
|
91
|
+
"message.contextRecall": "Session recall",
|
|
92
|
+
"message.retry.active": "Retrying model request",
|
|
93
|
+
"message.retry.cancelled": "Model request retry cancelled",
|
|
94
|
+
"message.retry.started": "Retried model request",
|
|
95
|
+
"message.retry.scheduled": "Waiting to retry model request",
|
|
96
|
+
"message.retry.status": "{label} ({retry}/{maximum}) · {seconds}s",
|
|
97
|
+
"message.retry.delay": "Retry delay: ",
|
|
98
|
+
"message.retry.failure": "Failure reason: ",
|
|
99
|
+
"bar.history": "History",
|
|
100
|
+
"bar.menu": "Menu",
|
|
101
|
+
"bar.new": "New chat",
|
|
102
|
+
"bar.newAria": "New chat",
|
|
103
|
+
"history.empty": "No other chats yet",
|
|
104
|
+
"history.untitled": "New chat",
|
|
105
|
+
"history.loading": "Loading chats…",
|
|
106
|
+
"chat.loading": "Loading conversation…",
|
|
107
|
+
"history.today": "Today",
|
|
108
|
+
"history.week": "Previous 7 days",
|
|
109
|
+
"history.older": "Older",
|
|
110
|
+
"agent.refresh": "Refresh",
|
|
111
|
+
"agent.loading": "Loading settings…",
|
|
112
|
+
"question.title": "Choose an option",
|
|
113
|
+
"question.waiting": "Waiting for the Host question channel…",
|
|
114
|
+
"question.busy": "Submitting…",
|
|
115
|
+
};
|
package/i18n/preview.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const ZH = {
|
|
2
|
+
hierarchy: "会话层级",
|
|
3
|
+
tabs: "工作区标签页",
|
|
4
|
+
chat: "对话",
|
|
5
|
+
preview: "文件预览",
|
|
6
|
+
produced: "产物",
|
|
7
|
+
mediaLoading: "正在准备媒体预览…",
|
|
8
|
+
openInTab: "在标签中打开",
|
|
9
|
+
download: "下载",
|
|
10
|
+
close: "关闭 {name}",
|
|
11
|
+
evicted: "已关闭最久未使用的文件 {name}",
|
|
12
|
+
empty: "选择一个文件进行预览",
|
|
13
|
+
loading: "正在加载文件…",
|
|
14
|
+
failed: "无法预览此文件",
|
|
15
|
+
retry: "重试",
|
|
16
|
+
truncated: "文件较大,仅显示前 1 MB 内容。",
|
|
17
|
+
unsupportedTitle: "暂不支持预览这种文件",
|
|
18
|
+
unsupported: "目前支持图片、PDF、音视频、Markdown 和文本文件。",
|
|
19
|
+
"error.file_not_found": "文件不存在或已被移动",
|
|
20
|
+
"error.file_changed": "文件在打开时发生变化,请重试",
|
|
21
|
+
"error.path_not_file": "这个路径不是文件,无法预览",
|
|
22
|
+
"error.file_not_text": "文件不是有效的 UTF-8 文本",
|
|
23
|
+
"error.file_too_large": "文件超过预览大小限制",
|
|
24
|
+
"error.path_forbidden": "不能预览工作区外的文件",
|
|
25
|
+
"error.workspace_not_found": "当前会话没有可用工作区",
|
|
26
|
+
"error.workspace_unavailable": "当前工作区不可用",
|
|
27
|
+
"error.file_preview_failed": "文件预览加载失败",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const EN = {
|
|
31
|
+
hierarchy: "Session hierarchy",
|
|
32
|
+
tabs: "Workspace tabs",
|
|
33
|
+
chat: "Chat",
|
|
34
|
+
preview: "File preview",
|
|
35
|
+
produced: "Produced",
|
|
36
|
+
mediaLoading: "Preparing media preview…",
|
|
37
|
+
openInTab: "Open in tab",
|
|
38
|
+
download: "Download",
|
|
39
|
+
close: "Close {name}",
|
|
40
|
+
evicted: "Closed least recently used file {name}",
|
|
41
|
+
empty: "Select a file to preview",
|
|
42
|
+
loading: "Loading file…",
|
|
43
|
+
failed: "Unable to preview this file",
|
|
44
|
+
retry: "Retry",
|
|
45
|
+
truncated: "This file is large; only the first 1 MB is shown.",
|
|
46
|
+
unsupportedTitle: "This file type cannot be previewed yet",
|
|
47
|
+
unsupported: "Images, PDF, audio, video, Markdown, and text files are supported.",
|
|
48
|
+
"error.file_not_found": "The file no longer exists",
|
|
49
|
+
"error.file_changed": "The file changed while it was opening; try again",
|
|
50
|
+
"error.path_not_file": "This path is not a file, so it cannot be previewed",
|
|
51
|
+
"error.file_not_text": "The file is not valid UTF-8 text",
|
|
52
|
+
"error.file_too_large": "The file exceeds the preview size limit",
|
|
53
|
+
"error.path_forbidden": "Files outside the workspace cannot be previewed",
|
|
54
|
+
"error.workspace_not_found": "No workspace is available for this session",
|
|
55
|
+
"error.workspace_unavailable": "The current workspace is unavailable",
|
|
56
|
+
"error.file_preview_failed": "File preview failed to load",
|
|
57
|
+
};
|
package/i18n/search.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const ZH = {
|
|
2
|
+
"settings.title": "网络搜索",
|
|
3
|
+
"settings.intro": "使用 Router 中已配置的搜索服务,无需在 Lares 中重复填写密钥。",
|
|
4
|
+
"settings.refresh": "刷新",
|
|
5
|
+
"settings.refreshing": "刷新中…",
|
|
6
|
+
"settings.router": "Router 控制台",
|
|
7
|
+
"settings.loading": "正在读取 Router 搜索服务…",
|
|
8
|
+
"settings.status.ready": "网络搜索可用 · 默认服务 {model}",
|
|
9
|
+
"settings.status.notReady": "网络搜索尚不可用,请先在 Olares Router 配置并选择默认搜索服务",
|
|
10
|
+
"settings.default": "默认搜索服务",
|
|
11
|
+
"settings.default.none": "未选择",
|
|
12
|
+
"settings.default.empty": "Router 中没有可用的搜索服务",
|
|
13
|
+
"settings.loadFailed": "读取 Router 搜索服务失败:{msg}",
|
|
14
|
+
"settings.refreshFailed": "刷新 Router 搜索服务失败:{msg}",
|
|
15
|
+
"settings.saveFailed": "设置默认搜索服务失败:{msg}",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const EN = {
|
|
19
|
+
"settings.title": "Web search",
|
|
20
|
+
"settings.intro": "Use a search service configured in Router without entering its credentials again.",
|
|
21
|
+
"settings.refresh": "Refresh",
|
|
22
|
+
"settings.refreshing": "Refreshing…",
|
|
23
|
+
"settings.router": "Router console",
|
|
24
|
+
"settings.loading": "Loading Router search services…",
|
|
25
|
+
"settings.status.ready": "Web search ready · default service {model}",
|
|
26
|
+
"settings.status.notReady": "Web search unavailable; configure and select a default service in Olares Router",
|
|
27
|
+
"settings.default": "Default search service",
|
|
28
|
+
"settings.default.none": "None",
|
|
29
|
+
"settings.default.empty": "No search service is available in Router",
|
|
30
|
+
"settings.loadFailed": "Failed to load Router search services: {msg}",
|
|
31
|
+
"settings.refreshFailed": "Failed to refresh Router search services: {msg}",
|
|
32
|
+
"settings.saveFailed": "Failed to set the default search service: {msg}",
|
|
33
|
+
};
|
package/i18n/t.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function interpolate(template, params = {}) {
|
|
2
|
+
return String(template).replace(/\{(\w+)\}/g, (_, name) =>
|
|
3
|
+
params[name] == null ? `{${name}}` : String(params[name]),
|
|
4
|
+
);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function langOf(locale) {
|
|
8
|
+
const tag = String(locale ?? "en").toLowerCase();
|
|
9
|
+
return tag === "zh" || tag.startsWith("zh") ? "zh" : "en";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function t(catalog, locale, key, params) {
|
|
13
|
+
const table = catalog[langOf(locale)] ?? catalog.en;
|
|
14
|
+
const raw = table?.[key] ?? catalog.en?.[key] ?? key;
|
|
15
|
+
return interpolate(raw, params);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function messageFromCode(translate, code, fallbackKey) {
|
|
19
|
+
const key = `error.${code}`;
|
|
20
|
+
const text = translate(key);
|
|
21
|
+
return text === key ? translate(fallbackKey) : text;
|
|
22
|
+
}
|
package/i18n/voice.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const ZH = {
|
|
2
|
+
"error.voice_permission_denied": "麦克风权限被拒绝",
|
|
3
|
+
"error.voice_audio_too_large": "录音过长",
|
|
4
|
+
"error.voice_audio_unreadable": "音频无法识别,请重录",
|
|
5
|
+
"error.voice_model_unavailable": "语音模型不可用,请先在 Olares 模型控制台安装语音识别应用",
|
|
6
|
+
"error.voice_timeout": "转写超时,请重试",
|
|
7
|
+
"error.voice_too_short": "录音太短",
|
|
8
|
+
"error.voice_no_input": "没有检测到声音",
|
|
9
|
+
"error.voice_failed": "转写失败,请重试",
|
|
10
|
+
"error.voice_unsupported": "当前浏览器不支持录音",
|
|
11
|
+
"mic.stop": "点击结束录音",
|
|
12
|
+
"mic.transcribing": "转写中…",
|
|
13
|
+
"mic.idle": "语音输入",
|
|
14
|
+
"mic.blocked": "当前无法输入",
|
|
15
|
+
"lang.auto": "自动检测",
|
|
16
|
+
"status.auto": "自动",
|
|
17
|
+
"settings.title": "语音输入",
|
|
18
|
+
"settings.intro": "在输入框旁点击麦克风录音,录完自动转写并填入文本。转写由 Olares Router 的语音模型提供。",
|
|
19
|
+
"settings.refresh": "刷新",
|
|
20
|
+
"settings.refreshing": "刷新中…",
|
|
21
|
+
"settings.router": "Router 控制台",
|
|
22
|
+
"settings.loading": "正在读取语音输入配置…",
|
|
23
|
+
"settings.loadFailed": "读取语音输入配置失败",
|
|
24
|
+
"settings.refreshFailed": "刷新语音模型列表失败",
|
|
25
|
+
"settings.status.ready": "语音就绪 · 模型 {model}",
|
|
26
|
+
"settings.status.notReady": "语音模型尚不可用,请先在 Olares 模型控制台安装语音识别应用",
|
|
27
|
+
"settings.model.title": "语音模型",
|
|
28
|
+
"settings.model.hint": "留空则自动选择目录中的语音模型",
|
|
29
|
+
"settings.model.auto": "自动选择",
|
|
30
|
+
"settings.language.title": "识别语言",
|
|
31
|
+
"settings.language.hint": "指定语言可提升准确度与速度",
|
|
32
|
+
"settings.saveFailed": "保存失败",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const EN = {
|
|
36
|
+
"error.voice_permission_denied": "Microphone access denied",
|
|
37
|
+
"error.voice_audio_too_large": "Recording too long",
|
|
38
|
+
"error.voice_audio_unreadable": "Audio unreadable, please re-record",
|
|
39
|
+
"error.voice_model_unavailable":
|
|
40
|
+
"Voice model unavailable; install a speech-recognition app in the Olares Model Console first",
|
|
41
|
+
"error.voice_timeout": "Transcription timed out, please retry",
|
|
42
|
+
"error.voice_too_short": "Recording too short",
|
|
43
|
+
"error.voice_no_input": "No voice detected",
|
|
44
|
+
"error.voice_failed": "Transcription failed, please retry",
|
|
45
|
+
"error.voice_unsupported": "This browser does not support recording",
|
|
46
|
+
"mic.stop": "Click to stop recording",
|
|
47
|
+
"mic.transcribing": "Transcribing…",
|
|
48
|
+
"mic.idle": "Voice input",
|
|
49
|
+
"mic.blocked": "Input unavailable",
|
|
50
|
+
"lang.auto": "Auto-detect",
|
|
51
|
+
"status.auto": "auto",
|
|
52
|
+
"settings.title": "Voice input",
|
|
53
|
+
"settings.intro":
|
|
54
|
+
"Click the microphone beside the composer to record; the take is transcribed and inserted automatically. Transcription is served by an Olares Router voice model.",
|
|
55
|
+
"settings.refresh": "Refresh",
|
|
56
|
+
"settings.refreshing": "Refreshing…",
|
|
57
|
+
"settings.router": "Router console",
|
|
58
|
+
"settings.loading": "Loading voice input settings…",
|
|
59
|
+
"settings.loadFailed": "Failed to load voice input settings",
|
|
60
|
+
"settings.refreshFailed": "Failed to refresh voice models",
|
|
61
|
+
"settings.status.ready": "Voice ready · model {model}",
|
|
62
|
+
"settings.status.notReady":
|
|
63
|
+
"Voice model not available yet; install a speech-recognition app in the Olares Model Console first",
|
|
64
|
+
"settings.model.title": "Voice model",
|
|
65
|
+
"settings.model.hint": "Leave empty to auto-pick a voice model from the catalog",
|
|
66
|
+
"settings.model.auto": "Auto",
|
|
67
|
+
"settings.language.title": "Recognition language",
|
|
68
|
+
"settings.language.hint": "Specifying a language improves accuracy and speed",
|
|
69
|
+
"settings.saveFailed": "Failed to save",
|
|
70
|
+
};
|
package/icons/mark.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PRODUCT_NAME } from "../brand/identity.js";
|
|
2
|
+
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
export const MARK_PATH = "/lares/mark.svg";
|
|
8
|
+
|
|
9
|
+
export const MARK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="${PRODUCT_NAME}">
|
|
10
|
+
<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"/>
|
|
14
|
+
</linearGradient>
|
|
15
|
+
</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"/>
|
|
19
|
+
</svg>`;
|
|
20
|
+
|
|
21
|
+
/** Inline form for CSS `background-image`, which cannot reference a route that may 404 mid-boot. */
|
|
22
|
+
export const MARK_DATA_URI = `url("data:image/svg+xml,${encodeURIComponent(MARK_SVG)}")`;
|
package/larepass/chat.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { isAuthFailure } from "./host.js";
|
|
2
|
+
import { consumeMux, MUX_PATH } from "./mux.js";
|
|
3
|
+
import { foldTranscript, mergeEvents } from "./transcript.js";
|
|
4
|
+
import { promptPayload, rpcPath, unwrapServerResponse, wrapClientRequest } from "./rpc.js";
|
|
5
|
+
|
|
6
|
+
function httpStatus(res) {
|
|
7
|
+
return Number(res?.status) || 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function asError(code, message, extra = {}) {
|
|
11
|
+
return { ok: false, error: { code, message }, ...extra };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function callRpc(request, method, payload = {}) {
|
|
15
|
+
const envelope = wrapClientRequest(method, payload);
|
|
16
|
+
let res;
|
|
17
|
+
try {
|
|
18
|
+
res = await request(rpcPath(method), { method: "POST", body: envelope });
|
|
19
|
+
} catch (err) {
|
|
20
|
+
return asError("unreachable", err instanceof Error ? err.message : String(err), { status: "unreachable" });
|
|
21
|
+
}
|
|
22
|
+
const http = httpStatus(res);
|
|
23
|
+
if (isAuthFailure(http)) {
|
|
24
|
+
return asError("unauthorized", `Host rejected this login (${http})`, { status: "unauthorized", http });
|
|
25
|
+
}
|
|
26
|
+
if (http < 200 || http >= 300) {
|
|
27
|
+
return asError("http", `Host returned ${http}`, { status: "error", http });
|
|
28
|
+
}
|
|
29
|
+
const parsed = unwrapServerResponse(res.body);
|
|
30
|
+
if (!parsed.ok) return { ...parsed, status: "rpc-error", http };
|
|
31
|
+
return { ...parsed, status: "ok", http };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function listRootSessions(rpc) {
|
|
35
|
+
const listed = await rpc("session.list", {});
|
|
36
|
+
if (!listed.ok) return listed;
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
status: listed.status || "ok",
|
|
40
|
+
value: { items: rootSessions(listed.value?.items).map(summarizeSession) },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function ensureSession(rpc) {
|
|
45
|
+
const listed = await listRootSessions(rpc);
|
|
46
|
+
if (!listed.ok) return listed;
|
|
47
|
+
const existing = listed.value.items[0];
|
|
48
|
+
if (existing) {
|
|
49
|
+
return { ok: true, status: "ok", value: { sessionId: existing.sessionId, sessions: listed.value.items } };
|
|
50
|
+
}
|
|
51
|
+
const created = await rpc("session.create", {});
|
|
52
|
+
if (!created.ok) return created;
|
|
53
|
+
const row = summarizeSession({ ...created.value, sessionId: created.value.sessionId });
|
|
54
|
+
return { ok: true, status: "ok", value: { sessionId: row.sessionId, sessions: [row] } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function rootSessions(items) {
|
|
58
|
+
return (Array.isArray(items) ? items : []).filter(
|
|
59
|
+
(row) => row?.sessionId && !row.parentSessionId && row.origin !== "subagent",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function basenameCwd(cwd) {
|
|
64
|
+
if (!cwd) return "";
|
|
65
|
+
const base = String(cwd).replace(/[/\\]+$/, "").split(/[/\\]/).pop();
|
|
66
|
+
return base && base !== "." ? base : "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function sessionTitleOf(row) {
|
|
70
|
+
const projected = row?.projections?.values?.title;
|
|
71
|
+
if (typeof projected === "string" && projected.trim()) return projected.trim();
|
|
72
|
+
if (typeof row?.title === "string" && row.title.trim()) return row.title.trim();
|
|
73
|
+
if (typeof row?.name === "string" && row.name.trim()) return row.name.trim();
|
|
74
|
+
return basenameCwd(row?.cwd);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function summarizeSession(row) {
|
|
78
|
+
return {
|
|
79
|
+
sessionId: row.sessionId,
|
|
80
|
+
title: sessionTitleOf(row),
|
|
81
|
+
updatedAt: Number(row.updatedAt || row.mtime || row.createdAt || 0) || 0,
|
|
82
|
+
blank: Boolean(row.blank),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function visibleHistorySessions(sessions, currentId) {
|
|
87
|
+
return (Array.isArray(sessions) ? sessions : []).filter(
|
|
88
|
+
(row) => row?.sessionId && (!row.blank || row.sessionId === currentId),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function startOfLocalDay(ms) {
|
|
93
|
+
const date = new Date(ms);
|
|
94
|
+
date.setHours(0, 0, 0, 0);
|
|
95
|
+
return date.getTime();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function groupSessionsByRecency(sessions, now = Date.now()) {
|
|
99
|
+
const today = startOfLocalDay(now);
|
|
100
|
+
const week = today - 7 * 24 * 60 * 60 * 1000;
|
|
101
|
+
const buckets = { today: [], week: [], older: [] };
|
|
102
|
+
const sorted = [...sessions].sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0));
|
|
103
|
+
for (const row of sorted) {
|
|
104
|
+
const at = Number(row.updatedAt) || 0;
|
|
105
|
+
if (at >= today) buckets.today.push(row);
|
|
106
|
+
else if (at >= week) buckets.week.push(row);
|
|
107
|
+
else buckets.older.push(row);
|
|
108
|
+
}
|
|
109
|
+
return [
|
|
110
|
+
{ id: "today", key: "history.today", rows: buckets.today },
|
|
111
|
+
{ id: "week", key: "history.week", rows: buckets.week },
|
|
112
|
+
{ id: "older", key: "history.older", rows: buckets.older },
|
|
113
|
+
].filter((section) => section.rows.length > 0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function loadTranscript(rpc, sessionId) {
|
|
117
|
+
const history = await rpc("session.history", { sessionId });
|
|
118
|
+
if (!history.ok) return history;
|
|
119
|
+
return { ok: true, status: "ok", value: foldTranscript(history.value?.events) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function clientTimeZone() {
|
|
123
|
+
try {
|
|
124
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
125
|
+
} catch {
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function sendPrompt(rpc, sessionId, text) {
|
|
131
|
+
return rpc("session.prompt", promptPayload(sessionId, text, clientTimeZone()));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { consumeMux, mergeEvents, MUX_PATH };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Same projection as dsh-client-runtime `contextProvenance` / `contextForm`. */
|
|
2
|
+
|
|
3
|
+
const KNOWN_FORMS = ["instructions", "catalog", "snapshot", "notice", "relay", "recall"];
|
|
4
|
+
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function readString(record, key) {
|
|
10
|
+
const value = record[key];
|
|
11
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function collect(source, member, field) {
|
|
15
|
+
const list = source[member];
|
|
16
|
+
if (!Array.isArray(list)) return [];
|
|
17
|
+
const seen = [];
|
|
18
|
+
for (const entry of list) {
|
|
19
|
+
const record = asRecord(entry);
|
|
20
|
+
const value = record === null ? null : readString(record, field);
|
|
21
|
+
if (value !== null && !seen.includes(value)) seen.push(value);
|
|
22
|
+
}
|
|
23
|
+
return seen;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function joined(names) {
|
|
27
|
+
return names.length > 0 ? names.join(", ") : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function sessionRecallLabels(source) {
|
|
31
|
+
const record = asRecord(source);
|
|
32
|
+
if (record === null || readString(record, "kind") !== "session-reference") return [];
|
|
33
|
+
return collect(record, "references", "label");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function contextProvenance(source) {
|
|
37
|
+
const record = asRecord(source);
|
|
38
|
+
const kind = record === null ? null : readString(record, "kind");
|
|
39
|
+
if (record === null || kind === null) {
|
|
40
|
+
return { role: "inject", label: null };
|
|
41
|
+
}
|
|
42
|
+
switch (kind) {
|
|
43
|
+
case "session-reference":
|
|
44
|
+
return { role: "recall", label: joined(collect(record, "references", "label")) ?? kind };
|
|
45
|
+
case "agent-instructions":
|
|
46
|
+
return { role: "inject", label: joined(collect(record, "changes", "path")) ?? kind };
|
|
47
|
+
case "plugin":
|
|
48
|
+
return { role: "inject", label: readString(record, "plugin") ?? kind };
|
|
49
|
+
case "skill-invocation":
|
|
50
|
+
return { role: "inject", label: readString(record, "name") ?? kind };
|
|
51
|
+
default:
|
|
52
|
+
return { role: "inject", label: kind };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function contextForm(source) {
|
|
57
|
+
const record = asRecord(source);
|
|
58
|
+
const form = record === null ? null : readString(record, "form");
|
|
59
|
+
return form !== null && KNOWN_FORMS.includes(form) ? form : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isHumanUserSource(source) {
|
|
63
|
+
const kind = asRecord(source) === null ? null : readString(asRecord(source), "kind");
|
|
64
|
+
return kind === null || kind === "user";
|
|
65
|
+
}
|
package/larepass/host.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const APP_ID = "lares";
|
|
2
|
+
export const LARES_API_PREFIX = "/api/lares";
|
|
3
|
+
export const MODELS_PATH = `${LARES_API_PREFIX}/models`;
|
|
4
|
+
export const PC_TEST_PROXY = "/laresHost";
|
|
5
|
+
|
|
6
|
+
/** Host plugin routes (`file-preview`, models, voice, …). Same path on PC and LarePass. */
|
|
7
|
+
export function isLaresPluginPath(path) {
|
|
8
|
+
const suffix = String(path ?? "").startsWith("/") ? path : `/${path}`;
|
|
9
|
+
return suffix === LARES_API_PREFIX
|
|
10
|
+
|| suffix.startsWith(`${LARES_API_PREFIX}/`)
|
|
11
|
+
|| suffix.startsWith(`${LARES_API_PREFIX}?`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function originOf(url) {
|
|
15
|
+
if (!url) return "";
|
|
16
|
+
try {
|
|
17
|
+
return new URL(String(url)).origin;
|
|
18
|
+
} catch {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function findLaresEntrance(apps) {
|
|
24
|
+
const list = Array.isArray(apps) ? apps : [];
|
|
25
|
+
const hit = list.find((app) => app?.appid === APP_ID || app?.id === APP_ID);
|
|
26
|
+
return originOf(hit?.url);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function entranceFromDomain({ protocol = "https:", subdomain, accountDomain }) {
|
|
30
|
+
const host = String(subdomain ?? "").trim();
|
|
31
|
+
const domain = String(accountDomain ?? "").trim();
|
|
32
|
+
if (!host || !domain) return "";
|
|
33
|
+
const scheme = String(protocol).replace(/:\/?\/?$/, "");
|
|
34
|
+
return `${scheme}://${host}.${domain}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function hostConfigFromEnv(env = {}) {
|
|
38
|
+
return {
|
|
39
|
+
baseUrl: entranceFromDomain({
|
|
40
|
+
protocol: env.PROTOCOL,
|
|
41
|
+
subdomain: env.LARES_SUB_DOMAIN,
|
|
42
|
+
accountDomain: env.ACCOUNT_DOMAIN,
|
|
43
|
+
}),
|
|
44
|
+
proxyPrefix: env.IS_PC_TEST ? PC_TEST_PROXY : "",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hostUrl({ baseUrl = "", proxyPrefix = "", path }) {
|
|
49
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
50
|
+
if (proxyPrefix) {
|
|
51
|
+
// Plugin routes are same-origin Host URLs. Prefixing them with /laresHost
|
|
52
|
+
// would make the same conversation emit different img/src on LarePass vs PC.
|
|
53
|
+
// RPC (`/api/session.list`) still needs the proxy: `/api` is Files.
|
|
54
|
+
if (isLaresPluginPath(suffix)) return suffix;
|
|
55
|
+
return `${proxyPrefix.replace(/\/$/, "")}${suffix}`;
|
|
56
|
+
}
|
|
57
|
+
const origin = String(baseUrl).replace(/\/$/, "");
|
|
58
|
+
return origin ? `${origin}${suffix}` : suffix;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isAuthFailure(http) {
|
|
62
|
+
return http === 401 || http === 403 || http === 301 || http === 302 || http === 303 || http === 307 || http === 308;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function probeHost(request, path = MODELS_PATH) {
|
|
66
|
+
try {
|
|
67
|
+
const res = await request(path);
|
|
68
|
+
const http = Number(res?.status) || 0;
|
|
69
|
+
if (isAuthFailure(http)) {
|
|
70
|
+
return { status: "unauthorized", http };
|
|
71
|
+
}
|
|
72
|
+
if (http >= 200 && http < 300) return { status: "ok", http, body: res.body };
|
|
73
|
+
return { status: "error", http };
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return {
|
|
76
|
+
status: "unreachable",
|
|
77
|
+
error: err instanceof Error ? err.message : String(err),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|