Skip to content

Commit dd476d7

Browse files
committed
feat: complete remote session UI integration for lion
1 parent e05fc71 commit dd476d7

34 files changed

Lines changed: 1503 additions & 1151 deletions

.env.development

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33

44
JMS_KOKO_DEV_URL=http://localhost:5050
55

6-
# JMS_LION_DEV_URL=http://localhost:8081
6+
# Lion 默认复用 JMS_KOKO_DEV_URL;仅兼容独立调试时覆盖
7+
# JMS_LION_DEV_URL=http://localhost:5050
78
# JMS_CHEN_DEV_URL=http://localhost:8082

nuxt.config.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const jumpServerTarget = process.env.JMS_CORE_DEV_URL || "http://localhost:8080";
22
const kokoTarget = process.env.JMS_KOKO_DEV_URL || "http://localhost:5050";
3-
const lionTarget = process.env.JMS_LION_DEV_URL || "http://localhost:8081";
3+
// JMS_LION_DEV_URL remains a compatibility override; Lion is served by Koko by default.
4+
const lionTarget = process.env.JMS_LION_DEV_URL || kokoTarget;
45
const chenTarget = process.env.JMS_CHEN_DEV_URL || "http://localhost:8082";
56
const faceliveTarget = process.env.JMS_FACELIVE_DEV_URL || "http://localhost:5173";
67
const kaelTarget = process.env.JMS_KAEL_DEV_URL || "http://localhost:5172";
@@ -163,13 +164,26 @@ export default defineNuxtConfig({
163164
rewrite: (path) => path.replace(/^\/luna/, ""),
164165
configure: bindProxyErrorHandler("luna-lion-ws")
165166
},
166-
"/luna/lion/": {
167+
"/luna/lion/api/": {
168+
target: lionTarget,
169+
secure: false,
170+
changeOrigin: true,
171+
rewrite: (path) => path.replace(/^\/luna/, ""),
172+
configure: bindProxyErrorHandler("luna-lion-api")
173+
},
174+
"/luna/lion/token/": {
175+
target: lionTarget,
176+
secure: false,
177+
changeOrigin: true,
178+
rewrite: (path) => path.replace(/^\/luna/, ""),
179+
configure: bindProxyErrorHandler("luna-lion-token")
180+
},
181+
"/luna/lion/health/": {
167182
target: lionTarget,
168183
secure: false,
169-
ws: true,
170184
changeOrigin: true,
171185
rewrite: (path) => path.replace(/^\/luna/, ""),
172-
configure: bindProxyErrorHandler("luna-lion-http")
186+
configure: bindProxyErrorHandler("luna-lion-health")
173187
},
174188
"/lion/ws/": {
175189
target: lionTarget.replace(/^http/i, "ws"),
@@ -178,12 +192,23 @@ export default defineNuxtConfig({
178192
changeOrigin: true,
179193
configure: bindProxyErrorHandler("lion-ws")
180194
},
181-
"/lion/": {
195+
"/lion/api/": {
196+
target: lionTarget,
197+
secure: false,
198+
changeOrigin: true,
199+
configure: bindProxyErrorHandler("lion-api")
200+
},
201+
"/lion/token/": {
202+
target: lionTarget,
203+
secure: false,
204+
changeOrigin: true,
205+
configure: bindProxyErrorHandler("lion-token")
206+
},
207+
"/lion/health/": {
182208
target: lionTarget,
183209
secure: false,
184-
ws: true,
185210
changeOrigin: true,
186-
configure: bindProxyErrorHandler("lion-http")
211+
configure: bindProxyErrorHandler("lion-health")
187212
},
188213
"/chen/ws/": {
189214
target: chenTarget.replace(/^http/i, "ws"),

src-tauri/capabilities/main.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"window-state:default",
8585
"clipboard-manager:allow-read-text",
8686
"clipboard-manager:allow-write-text",
87+
"clipboard-manager:allow-write-image",
8788
"updater:allow-check",
8889
"updater:allow-download",
8990
"updater:allow-install",
Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,28 @@
11
<script setup lang="ts">
2-
import { message as lionMessages } from "@/lion/locales/modules";
3-
import { withBasePath } from "@/lion/utils/base";
4-
import { LanguageCode } from "@/lion/utils/config";
2+
import { message as lionMessages, loadRemoteTranslations } from "@/lion/locales";
53
64
import "@/lion/styles/base.css";
75
86
const { mergeLocaleMessage } = useI18n();
97
10-
const loaded = ref(false);
11-
const normalizedLangCode = LanguageCode.toLowerCase();
8+
for (const [code, value] of Object.entries(lionMessages)) {
9+
mergeLocaleMessage(code, value);
10+
}
1211
1312
onMounted(async () => {
14-
for (const [code, value] of Object.entries(lionMessages)) {
15-
mergeLocaleMessage(code, value);
16-
}
17-
1813
try {
19-
const response = await fetch(
20-
`${withBasePath("/api/v1/settings/i18n/lion/")}?lang=${encodeURIComponent(normalizedLangCode)}&flat=0`,
21-
{
22-
credentials: "include"
23-
}
24-
);
25-
26-
if (response.ok) {
27-
const translations = await response.json();
28-
for (const [key, value] of Object.entries(translations)) {
29-
mergeLocaleMessage(key, value as Record<string, any>);
30-
}
14+
const translations = await loadRemoteTranslations();
15+
for (const [key, value] of Object.entries(translations)) {
16+
mergeLocaleMessage(key, value);
3117
}
3218
} catch (error) {
3319
console.error("load lion i18n failed", error);
34-
} finally {
35-
loaded.value = true;
3620
}
3721
});
3822
</script>
3923

4024
<template>
41-
<div v-if="loaded" class="h-full w-full overflow-hidden">
25+
<div class="lion-surface h-full w-full overflow-hidden">
4226
<slot />
4327
</div>
4428
</template>

ui/lion/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
Lion 是 RDP/VNC 远程桌面 connector,结构与 koko 对齐:
44

5-
- `views/` — 独立页面(`/lion/connect``/lion/share``/lion/monitor`
5+
- `views/` — 独立页面(`/lion/connect``/lion/share/:id``/lion/monitor`
66
- `workspaces/` — 内嵌到 Luna workspace 的 session surface
77
- `shared/connectors/capabilities.ts` — 声明 `web_rdp_native` 连接方式
88
- `shared/connectors/registry.ts` — 解析到 `RemoteSessionSurface.vue`
99

10-
开发代理见根目录 `nuxt.config.ts``/lion/``/lion/ws/` 转发到 `localhost:8081`API/WS 固定走站点根路径 `/lion/*`(不带 `/luna` 前缀)。
10+
开发代理见根目录 `nuxt.config.ts`Lion 的 API、Token、Health 和 WebSocket 路径默认转发到 Koko(`JMS_KOKO_DEV_URL`,默认 `localhost:5050`)。`JMS_LION_DEV_URL` 仅作为兼容覆盖保留。页面路径由 Client 自己渲染,不转发给 Koko;API/WS 固定走站点根路径 `/lion/*`(不带 `/luna` 前缀)。

ui/lion/api/index.ts

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,102 @@
1-
import { withBasePath, withLionUrl } from "@/lion/utils/base";
1+
import { apiRequest } from "@/composables/useApiRequest";
2+
import { withLionUrl } from "@/lion/utils/base";
23

3-
const postJson = (url: string, data: any) => {
4-
return fetch(url, {
4+
export interface SuggestionUser {
5+
id: string;
6+
name: string;
7+
username: string;
8+
}
9+
10+
export interface SuggestionUserPage {
11+
count?: number;
12+
next?: string | null;
13+
previous?: string | null;
14+
results: SuggestionUser[];
15+
}
16+
17+
export interface ShareApiResponse {
18+
success?: boolean;
19+
message?: string;
20+
id?: string;
21+
verify_code?: string;
22+
[key: string]: unknown;
23+
}
24+
25+
export interface ShareSessionResponse extends ShareApiResponse {
26+
action_permission?: {
27+
value?: string;
28+
};
29+
session?: {
30+
id?: string;
31+
[key: string]: unknown;
32+
};
33+
}
34+
35+
export interface LionRequestAuth {
36+
ticket?: string;
37+
token?: string;
38+
}
39+
40+
const withRequestAuth = (url: string, auth?: LionRequestAuth) => {
41+
if (!auth?.ticket && !auth?.token) return url;
42+
const target = new URL(url, window.location.origin);
43+
if (auth.ticket) target.searchParams.set("ticket", auth.ticket);
44+
if (auth.token) target.searchParams.set("token", auth.token);
45+
return target.toString();
46+
};
47+
48+
const parseResponse = async <T>(response: Response): Promise<T> => {
49+
const text = await response.text();
50+
let data: unknown = null;
51+
52+
if (text) {
53+
try {
54+
data = JSON.parse(text);
55+
} catch {
56+
data = text;
57+
}
58+
}
59+
60+
if (!response.ok) {
61+
const message =
62+
data && typeof data === "object" && "message" in data
63+
? String((data as { message?: unknown }).message || "")
64+
: text;
65+
throw new Error(message || `HTTP ${response.status}`);
66+
}
67+
68+
return data as T;
69+
};
70+
71+
const postJson = async <T>(url: string, data: unknown): Promise<T> => {
72+
const response = await fetch(url, {
573
method: "POST",
674
credentials: "include",
775
headers: {
876
"Content-Type": "application/json"
977
},
1078
body: JSON.stringify(data)
1179
});
12-
};
1380

14-
const getSuggestionUsers = (query: string) => {
15-
const url = new URL(withBasePath("/api/v1/users/users/suggestions/"), window.location.origin);
16-
url.searchParams.set("search", query);
17-
return fetch(url.toString(), { credentials: "include" });
81+
return await parseResponse<T>(response);
1882
};
1983

20-
const createShareURL = (data: any, endpointUrl?: string) => postJson(withLionUrl("/api/share/", endpointUrl), data);
21-
const getShareSession = (id: string, data: any, endpointUrl?: string) =>
22-
postJson(withLionUrl(`/api/share/${id}/`, endpointUrl), data);
23-
const removeShareUser = (data: any, endpointUrl?: string) =>
24-
postJson(withLionUrl("/api/share/remove/", endpointUrl), data);
84+
const getSuggestionUsers = (query: string, page = 1, limit = 10) =>
85+
apiRequest<SuggestionUserPage | SuggestionUser[]>({
86+
method: "GET",
87+
path: "/api/v1/users/users/suggestions/",
88+
query: {
89+
search: query,
90+
page,
91+
limit
92+
}
93+
});
94+
95+
const createShareURL = (data: unknown, endpointUrl?: string, auth?: LionRequestAuth) =>
96+
postJson<ShareApiResponse>(withRequestAuth(withLionUrl("/api/share/", endpointUrl), auth), data);
97+
const getShareSession = (id: string, data: unknown, endpointUrl?: string, auth?: LionRequestAuth) =>
98+
postJson<ShareSessionResponse>(withRequestAuth(withLionUrl(`/api/share/${id}/`, endpointUrl), auth), data);
99+
const removeShareUser = (data: unknown, endpointUrl?: string, auth?: LionRequestAuth) =>
100+
postJson<ShareApiResponse>(withRequestAuth(withLionUrl("/api/share/remove/", endpointUrl), auth), data);
25101

26102
export { createShareURL, getShareSession, getSuggestionUsers, removeShareUser };

ui/lion/components/CardContainer/index.vue

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,11 @@ const expanded = ref(true);
88

99
<template>
1010
<UCard :ui="{ body: 'p-4' }">
11-
<button
12-
type="button"
13-
class="flex w-full items-center justify-between gap-2 text-left"
14-
@click="expanded = !expanded"
15-
>
11+
<UButton block color="neutral" variant="ghost" class="justify-between px-0 text-left" @click="expanded = !expanded">
1612
<slot v-if="!title" name="custom-header" />
1713
<span v-else class="text-sm font-medium">{{ title }}</span>
1814
<UIcon :name="expanded ? 'i-lucide-chevron-down' : 'i-lucide-chevron-left'" class="size-4" />
19-
</button>
15+
</UButton>
2016

2117
<div v-show="expanded" class="mt-3">
2218
<slot />

ui/lion/components/ClipBoardText.vue

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,58 @@
11
<script lang="ts" setup>
22
import { useDebounceFn } from "@vueuse/core";
3-
import { ref } from "vue";
3+
import { computed, ref } from "vue";
44
import { useI18n } from "vue-i18n";
55
import CardContainer from "@/lion/components/CardContainer/index.vue";
66
import { readClipboardText } from "@/lion/utils/clipboard";
77
88
const props = defineProps<{
99
remoteText?: string;
1010
disabled?: boolean;
11+
copyDisabled?: boolean;
12+
pasteDisabled?: boolean;
13+
pastePolicyDisabled?: boolean;
14+
textLimit?: number;
1115
}>();
1216
const emit = defineEmits(["update:text"]);
1317
const { t } = useI18n();
18+
const toast = useToast();
1419
1520
const inputValue = ref<string>("");
1621
const isLoading = ref<boolean>(false);
1722
const showRemoteText = ref<boolean>(false);
23+
const maxLength = computed(() => (props.textLimit && props.textLimit > 0 ? props.textLimit : undefined));
24+
const inputLength = computed(() => Array.from(inputValue.value).length);
25+
26+
const validateText = (text: string) => {
27+
if (props.pastePolicyDisabled) {
28+
toast.add({ title: t("ClipboardPasteDeniedByPolicy"), color: "warning" });
29+
return false;
30+
}
31+
if (props.disabled || props.pasteDisabled) {
32+
toast.add({ title: `${t("Paste")} ${t("NoPermission")}`, color: "warning" });
33+
return false;
34+
}
35+
if (maxLength.value && Array.from(text).length > maxLength.value) {
36+
toast.add({ title: `${t("Paste")} ${t("ClipboardTextLimitExceeded")}: ${maxLength.value}`, color: "warning" });
37+
return false;
38+
}
39+
return true;
40+
};
1841
1942
const handleInput = useDebounceFn((value: string) => {
43+
if (!validateText(value)) return;
2044
emit("update:text", value);
2145
}, 300);
2246
2347
const loadClipboardText = async () => {
2448
try {
2549
isLoading.value = true;
2650
const text = await readClipboardText();
51+
if (!validateText(text)) return;
2752
inputValue.value = text;
2853
await handleInput(text);
2954
} catch (error) {
30-
console.log("Failed to read clipboard text:", error);
55+
console.debug("Failed to read clipboard text:", error);
3156
} finally {
3257
isLoading.value = false;
3358
}
@@ -48,28 +73,31 @@ const handleFocus = async () => {
4873
<CardContainer :title="t('Clipboard')">
4974
<div class="mb-3 flex items-center justify-between gap-3">
5075
<span class="text-sm">{{ t("ShowRemoteClip") }}</span>
51-
<USwitch v-model="showRemoteText" :disabled="props.disabled" />
76+
<USwitch v-model="showRemoteText" :disabled="props.disabled || props.copyDisabled" />
5277
</div>
5378

5479
<UTextarea
5580
v-model="inputValue"
5681
:rows="4"
57-
:maxlength="4096"
5882
autoresize
59-
:disabled="props.disabled"
83+
:disabled="props.disabled || props.pasteDisabled || props.pastePolicyDisabled"
6084
:placeholder="t('AutoPasteOnClick')"
6185
class="w-full"
6286
@update:model-value="handleInput"
6387
@focus="handleFocus"
6488
/>
89+
<div class="mt-1 text-right text-xs text-muted">
90+
{{ inputLength }}
91+
<template v-if="maxLength">/ {{ maxLength }}</template>
92+
</div>
6593

6694
<UTextarea
6795
v-if="showRemoteText"
6896
:model-value="props.remoteText"
6997
:rows="4"
7098
readonly
7199
autoresize
72-
:disabled="props.disabled"
100+
:disabled="props.disabled || props.copyDisabled"
73101
class="mt-3 w-full"
74102
/>
75103
</CardContainer>

0 commit comments

Comments
 (0)