@olares/lares-mobile 0.15.0 → 0.15.2

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 CHANGED
@@ -4,16 +4,22 @@ LarePass 只依赖这一个包。对话、Host、设置都在内部走 `@olares/
4
4
 
5
5
  ```vue
6
6
  <template>
7
- <LaresApp :locale="locale" :env="env" />
7
+ <LaresApp :locale="locale" :device="device" v-bind="ports" />
8
8
  </template>
9
9
  <script setup>
10
- import { LaresApp } from "@olares/lares-mobile";
10
+ import { LaresApp, laresPortsFromAccount, findLaresEntrance } from "@olares/lares-mobile";
11
11
  </script>
12
12
  ```
13
13
 
14
+ `device` 为 `desktop` 时对话列居中、宽 748px;`mobile`(默认)保持全宽。
15
+
14
16
  设置页同样只挂 `LaresAgentSettings`。
15
17
 
16
- `env` 由宿主注入(`PROTOCOL` / `LARES_SUB_DOMAIN` / `ACCOUNT_DOMAIN` / `IS_PC_TEST`)。PC 预览走 `/laresHost` 代理时设 `IS_PC_TEST`。需要自定义鉴权请求时再传 `request`。
18
+ 正式 App:把当前登录账号的 `myApps` 交给 `laresPortsFromAccount`(或自己调 `findLaresEntrance` 得到 `baseUrl`)。入口前缀是安装时随机的,不要拼 `lares.<用户>.olares.com`,也不要把 webpack `.env` 里的调试子域带进生产。
19
+
20
+ PC 预览:`IS_PC_TEST` 时走 `/laresHost` 代理,`laresPortsFromAccount` 会继续用 `PROTOCOL` / `LARES_SUB_DOMAIN` / `ACCOUNT_DOMAIN`。自定义鉴权请求再传 `request`。
21
+
22
+ 切账号时更新 `baseUrl` / `env` 即可,客户端会丢掉上一台的 runtime 和设置缓存,改连新 Host。
17
23
 
18
24
  本地联调(包未上 registry 时):
19
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olares/lares-mobile",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Lares chat component for LarePass",
5
5
  "type": "module",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "vue": "^3.2.0"
29
29
  },
30
30
  "dependencies": {
31
- "@olares/lares-core": "0.15.0",
31
+ "@olares/lares-core": "0.15.2",
32
32
  "markdown-it": "^14.3.0"
33
33
  }
34
34
  }
package/src/App.vue CHANGED
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="lares-shell">
2
+ <div class="lares-shell" :data-device="chatDevice">
3
3
  <LaresChatBar
4
4
  :t="t"
5
5
  :title="sessionTitle"
@@ -16,52 +16,54 @@
16
16
  @close="panel = ''"
17
17
  @pick="pickSession"
18
18
  />
19
- <p v-if="failed" class="lares-shell__status">{{ failText }}</p>
20
- <button v-if="failed" type="button" class="lares-shell__retry" :disabled="starting" @click="retry">
21
- {{ t("probe.retry") }}
22
- </button>
23
- <LaresTranscript
24
- ref="transcript"
25
- :items="viewItems"
26
- :running="running"
27
- :loading="historyLoading || starting"
28
- :previews="previews"
29
- :media-url="mediaUrl"
30
- :scroll-top="scrollTop"
31
- :remember-scroll="rememberScroll"
32
- :sticking="sticking"
33
- :session-id="sessionId"
34
- :question="question"
35
- :question-busy="questionBusy"
36
- :t="t"
37
- @open="openFile"
38
- @media="pinLog"
39
- @answer="answerQuestion"
40
- />
41
- <LaresComposer
42
- :draft="draft"
43
- :sending="sending"
44
- :can-send="canSend"
45
- :model-label="modelLabel"
46
- :model-busy="modelBusy"
47
- :effort-label="effortLabel"
48
- :effort-disabled="effortDisabled"
49
- :attach-pending="upload.pending > 0"
50
- :attach-disabled="!sessionId || historyLoading || starting"
51
- :voice-phase="voicePhase"
52
- :voice-elapsed="voiceElapsed"
53
- :voice-error="voiceErrorText"
54
- :failures="upload.failures"
55
- :t="t"
56
- @update:draft="draft = $event"
57
- @send="send"
58
- @model="openModelSheet"
59
- @effort="openEffortSheet"
60
- @files="attachFiles"
61
- @voice="toggleVoice"
62
- @hold-start="onHoldStart"
63
- @hold-end="onHoldEnd"
64
- />
19
+ <div class="lares-shell__conversation">
20
+ <p v-if="failed" class="lares-shell__status">{{ failText }}</p>
21
+ <button v-if="failed" type="button" class="lares-shell__retry" :disabled="starting" @click="retry">
22
+ {{ t("probe.retry") }}
23
+ </button>
24
+ <LaresTranscript
25
+ ref="transcript"
26
+ :items="viewItems"
27
+ :running="running"
28
+ :loading="historyLoading || starting"
29
+ :previews="previews"
30
+ :media-url="mediaUrl"
31
+ :scroll-top="scrollTop"
32
+ :remember-scroll="rememberScroll"
33
+ :sticking="sticking"
34
+ :session-id="sessionId"
35
+ :question="question"
36
+ :question-busy="questionBusy"
37
+ :t="t"
38
+ @open="openFile"
39
+ @media="pinLog"
40
+ @answer="answerQuestion"
41
+ />
42
+ <LaresComposer
43
+ :draft="draft"
44
+ :sending="sending"
45
+ :can-send="canSend"
46
+ :model-label="modelLabel"
47
+ :model-busy="modelBusy"
48
+ :effort-label="effortLabel"
49
+ :effort-disabled="effortDisabled"
50
+ :attach-pending="upload.pending > 0"
51
+ :attach-disabled="!sessionId || historyLoading || starting"
52
+ :voice-phase="voicePhase"
53
+ :voice-elapsed="voiceElapsed"
54
+ :voice-error="voiceErrorText"
55
+ :failures="upload.failures"
56
+ :t="t"
57
+ @update:draft="draft = $event"
58
+ @send="send"
59
+ @model="openModelSheet"
60
+ @effort="openEffortSheet"
61
+ @files="attachFiles"
62
+ @voice="toggleVoice"
63
+ @hold-start="onHoldStart"
64
+ @hold-end="onHoldEnd"
65
+ />
66
+ </div>
65
67
  <LaresSheet :open="modelSheet" :title="t('model.menuAria')" @close="modelSheet = false">
66
68
  <p v-if="modelError" class="lares-shell__status">{{ modelError }}</p>
67
69
  <p v-else-if="!models" class="lares-shell__hint">{{ t("agent.loading") }}</p>
@@ -118,6 +120,7 @@ import { DEFAULT_MAX_UPLOAD_BYTES } from "@olares/lares-core/files/limits";
118
120
  import { groupModelsByProvider, effortMenuRows, reasoningOfModel, currentEffortId, selectionKey } from "@olares/lares-core/router/session-model";
119
121
  import { messageFromCode } from "@olares/lares-core/i18n/t";
120
122
  import { parseAskUserQuestions, singleSelectAnswer } from "@olares/lares-core/larepass/questions";
123
+ import { chatDevice as resolveChatDevice } from "./layout.js";
121
124
  import LaresChatBar from "./chat/ChatBar.vue";
122
125
  import LaresHistoryPanel from "./chat/HistoryPanel.vue";
123
126
  import LaresTranscript from "./chat/Transcript.vue";
@@ -132,6 +135,7 @@ export default {
132
135
  components: { LaresChatBar, LaresHistoryPanel, LaresTranscript, LaresComposer, LaresPreview, LaresSheet, LaresSettingRow },
133
136
  props: {
134
137
  locale: { type: String, default: "en" },
138
+ device: { type: String, default: "mobile" },
135
139
  baseUrl: { type: String, default: undefined },
136
140
  proxyPrefix: { type: String, default: undefined },
137
141
  request: { type: Function, default: undefined },
@@ -141,6 +145,7 @@ export default {
141
145
  return {
142
146
  sending: false,
143
147
  starting: false,
148
+ hostGen: 0,
144
149
  running: false,
145
150
  sessionId: "",
146
151
  draft: "",
@@ -183,9 +188,15 @@ export default {
183
188
  runtime() {
184
189
  return connectChat(this.ports);
185
190
  },
191
+ hostKey() {
192
+ return this.runtime.key;
193
+ },
186
194
  t() {
187
195
  return createT(this.locale);
188
196
  },
197
+ chatDevice() {
198
+ return resolveChatDevice(this.device);
199
+ },
189
200
  canSend() {
190
201
  return Boolean(this.draft.trim()) && Boolean(this.sessionId) && !this.running && !this.historyLoading && this.upload.pending === 0;
191
202
  },
@@ -274,6 +285,9 @@ export default {
274
285
  sessionId(id) {
275
286
  this.bindIntake(id);
276
287
  },
288
+ hostKey() {
289
+ this.switchHost();
290
+ },
277
291
  },
278
292
  created() {
279
293
  this.intake = new FileIntake((file, sessionId, options) => this.runtime.upload(file, options, sessionId));
@@ -290,7 +304,7 @@ export default {
290
304
  },
291
305
  });
292
306
  this.bindIntake(this.sessionId);
293
- this.unsub = this.runtime.subscribe((snap) => this.applySnap(snap));
307
+ this.bindRuntime();
294
308
  try {
295
309
  const raw = JSON.parse(localStorage.getItem("lares.mobile.effort") || "{}");
296
310
  if (raw && typeof raw === "object") this.effortByModel = raw;
@@ -404,17 +418,47 @@ export default {
404
418
  this.preview = { path: "", status: "idle", data: null, error: "" };
405
419
  },
406
420
  async retry() {
421
+ const gen = ++this.hostGen;
407
422
  this.starting = true;
408
423
  try {
409
424
  await this.runtime.start();
425
+ if (gen !== this.hostGen) return;
410
426
  await this.loadComposerMeta();
411
427
  } finally {
412
- this.starting = false;
428
+ if (gen === this.hostGen) this.starting = false;
413
429
  }
414
430
  },
415
431
  modelKey(model) {
416
432
  return selectionKey({ provider: model.provider, model: model.id });
417
433
  },
434
+ bindRuntime() {
435
+ this.unsub?.();
436
+ this.unsub = this.runtime.subscribe((snap) => this.applySnap(snap));
437
+ },
438
+ switchHost() {
439
+ this.voiceCap?.stop?.(true);
440
+ this.intakeUnsub?.();
441
+ if (this.sessionId) this.intake?.cancelSession(this.sessionId);
442
+ this.sessionId = "";
443
+ this.items = [];
444
+ this.sessions = [];
445
+ this.sessionsReady = false;
446
+ this.previews = {};
447
+ this.preview = { path: "", status: "idle", data: null, error: "" };
448
+ this.pendingUser = "";
449
+ this.failed = "";
450
+ this.error = "";
451
+ this.models = null;
452
+ this.modelError = "";
453
+ this.modelPending = "";
454
+ this.modelSheet = false;
455
+ this.panel = "";
456
+ this.question = null;
457
+ this.questionBusy = false;
458
+ this.bindIntake("");
459
+ this.bindRuntime();
460
+ this.retry();
461
+ },
418
462
  bindIntake(sessionId) {
419
463
  this.intakeUnsub?.();
420
464
  this.intakeUnsub = null;
@@ -68,7 +68,6 @@
68
68
  </svg>
69
69
  </button>
70
70
  <button
71
- v-if="showSend"
72
71
  type="submit"
73
72
  class="lares-composer__icon"
74
73
  data-kind="send"
@@ -79,8 +78,9 @@
79
78
  <path d="M12 19V7M6.5 12.5 12 7l5.5 5.5" />
80
79
  </svg>
81
80
  </button>
81
+ <!-- Voice input parked.
82
82
  <button
83
- v-else
83
+ v-if="!showSend"
84
84
  type="button"
85
85
  class="lares-composer__icon"
86
86
  data-kind="voice"
@@ -99,10 +99,11 @@
99
99
  <path d="M17.5 9v6" />
100
100
  </svg>
101
101
  </button>
102
+ -->
102
103
  </div>
103
104
  </div>
104
105
  </div>
105
- <p v-if="voiceError" class="lares-composer__hint" data-status="error">{{ voiceError }}</p>
106
+ <!-- <p v-if="voiceError" class="lares-composer__hint" data-status="error">{{ voiceError }}</p> -->
106
107
  <p v-for="item in failures" :key="item.id" class="lares-composer__hint" data-status="error">
107
108
  {{ t("upload.failed", { name: item.name, reason: t(`error.${item.code}`) }) }}
108
109
  </p>
@@ -187,6 +188,8 @@ export default {
187
188
  this.onSend();
188
189
  },
189
190
  canHold(event) {
191
+ // Voice input parked (hold-to-record).
192
+ return false;
190
193
  if (this.draft.trim() || this.sending || this.voiceBlocked) return false;
191
194
  if (event.pointerType === "mouse") return false;
192
195
  return true;
@@ -23,11 +23,15 @@
23
23
  playsinline
24
24
  />
25
25
  <audio
26
- v-else
26
+ v-else-if="item.kind === 'audio'"
27
27
  class="lares-turn-media-audio"
28
28
  :src="mediaUrl(item)"
29
29
  controls
30
30
  />
31
+ <div
32
+ v-else-if="item.kind === 'model3d'"
33
+ class="lares-turn-media-model3d"
34
+ >{{ t("model3dHint") }}</div>
31
35
  </figure>
32
36
  </div>
33
37
  <div v-if="group.files.length" class="lares-turn-files">
@@ -136,6 +140,14 @@ export default {
136
140
  width: min(520px, 100%);
137
141
  }
138
142
 
143
+ .lares-turn-media-model3d {
144
+ width: min(520px, 100%);
145
+ padding: 16px;
146
+ color: var(--q-ink-3);
147
+ font-size: 13px;
148
+ line-height: 20px;
149
+ }
150
+
139
151
  .lares-turn-files {
140
152
  display: grid;
141
153
  grid-template-columns: max-content minmax(0, 1fr);
package/src/host.js CHANGED
@@ -1,4 +1,4 @@
1
- import { hostConfigFromEnv, hostUrl, MODELS_PATH, probeHost } from "@olares/lares-core/larepass/host";
1
+ import { hostTarget, hostUrl, MODELS_PATH, probeHost } from "@olares/lares-core/larepass/host";
2
2
  import { callRpc, consumeMux, ensureSession, loadTranscript, MUX_PATH, sendPrompt } from "@olares/lares-core/larepass/chat";
3
3
  import { muxWsUrl } from "@olares/lares-core/larepass/mux";
4
4
  import { RESPOND_PATH } from "@olares/lares-core/larepass/rpc";
@@ -92,9 +92,7 @@ export async function defaultRequest(url, init = {}) {
92
92
  }
93
93
 
94
94
  export function createHostClient(ports = {}) {
95
- const fromEnv = hostConfigFromEnv(ports.env);
96
- const baseUrl = ports.baseUrl ?? fromEnv.baseUrl;
97
- const proxyPrefix = ports.proxyPrefix ?? fromEnv.proxyPrefix;
95
+ const { baseUrl, proxyPrefix } = hostTarget(ports);
98
96
  const send = ports.request ?? defaultRequest;
99
97
  const urlFor = (path) => hostUrl({ baseUrl, proxyPrefix, path });
100
98
  const request = (path, init) => send(urlFor(path), init);
package/src/index.d.ts CHANGED
@@ -29,11 +29,24 @@ export type HostPorts = {
29
29
 
30
30
  export const PC_TEST_PROXY: string;
31
31
 
32
+ export function findLaresEntrance(apps?: unknown[]): string;
33
+
32
34
  export function hostConfigFromEnv(env?: HostEnv): {
33
35
  baseUrl: string;
34
36
  proxyPrefix: string;
35
37
  };
36
38
 
39
+ export function hostKey(ports?: HostPorts): string;
40
+
41
+ export function laresPortsFromAccount(input?: {
42
+ env?: HostEnv;
43
+ apps?: unknown[];
44
+ }): {
45
+ baseUrl?: string;
46
+ env?: HostEnv;
47
+ proxyPrefix?: string;
48
+ };
49
+
37
50
  export function defaultRequest(url: string, init?: HostRequestInit): Promise<HostResponse>;
38
51
 
39
52
  export function createHostClient(ports?: HostPorts): {
@@ -44,8 +57,11 @@ export function createHostClient(ports?: HostPorts): {
44
57
  prompt: (sessionId: string, text: string) => Promise<{ ok: boolean }>;
45
58
  };
46
59
 
60
+ export type ChatDevice = "desktop" | "mobile";
61
+
47
62
  export const LaresApp: DefineComponent<{
48
63
  locale?: string;
64
+ device?: ChatDevice;
49
65
  baseUrl?: string;
50
66
  proxyPrefix?: string;
51
67
  env?: HostEnv;
package/src/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  export { default as LaresApp } from "./App.vue";
2
2
  export { default as LaresAgentSettings } from "./settings/AgentSettings.vue";
3
3
  export { createHostClient, defaultRequest } from "./host.js";
4
- export { hostConfigFromEnv, PC_TEST_PROXY } from "@olares/lares-core/larepass/host";
4
+ export {
5
+ findLaresEntrance,
6
+ hostConfigFromEnv,
7
+ hostKey,
8
+ laresPortsFromAccount,
9
+ PC_TEST_PROXY,
10
+ } from "@olares/lares-core/larepass/host";
package/src/layout.js ADDED
@@ -0,0 +1,8 @@
1
+ export const DEVICE_MOBILE = "mobile";
2
+ export const DEVICE_DESKTOP = "desktop";
3
+ export const DESKTOP_CHAT_WIDTH_PX = 748;
4
+
5
+ /** Host-facing layout. Unknown values stay on the phone column. */
6
+ export function chatDevice(value) {
7
+ return value === DEVICE_DESKTOP ? DEVICE_DESKTOP : DEVICE_MOBILE;
8
+ }
@@ -38,6 +38,9 @@
38
38
  @click="onMarkdownClick"
39
39
  />
40
40
  <pre v-else-if="data?.kind === 'text'" class="lares-preview__text">{{ data.text }}</pre>
41
+ <div v-else-if="data?.kind === 'model3d'" class="lares-preview__hint">
42
+ <p>{{ t("model3dHint") }}</p>
43
+ </div>
41
44
  <div v-else class="lares-preview__hint">
42
45
  <p>{{ t("unsupportedTitle") }}</p>
43
46
  <p>{{ t("unsupported") }}</p>
package/src/runtime.js CHANGED
@@ -1,12 +1,32 @@
1
+ import { hostKey } from "@olares/lares-core/larepass/host";
2
+ import { resetHostSettingsCache } from "@olares/lares-core/larepass/settings";
1
3
  import { createChatRuntime } from "@olares/lares-core/larepass/runtime";
2
4
  import { createHostClient } from "./host.js";
3
5
 
4
6
  let current = null;
7
+ let liveKey = "";
8
+
9
+ export function adoptHost(ports) {
10
+ const key = hostKey(ports);
11
+ if (key === liveKey) return key;
12
+ liveKey = key;
13
+ current?.dispose?.();
14
+ current = null;
15
+ resetHostSettingsCache();
16
+ return key;
17
+ }
5
18
 
6
19
  export function connectChat(ports) {
7
20
  const client = createHostClient(ports);
8
- const key = client.urlFor("/api");
21
+ const key = adoptHost(ports);
9
22
  if (current?.key === key) return current;
10
23
  current = Object.assign(createChatRuntime(client), { key });
11
24
  return current;
12
25
  }
26
+
27
+ export function resetChatHost() {
28
+ liveKey = "";
29
+ current?.dispose?.();
30
+ current = null;
31
+ resetHostSettingsCache();
32
+ }
@@ -22,6 +22,7 @@
22
22
  </p>
23
23
  <p v-if="modelError" class="lares-agent__hint" data-status="error">{{ modelError }}</p>
24
24
  </section>
25
+ <!-- Voice input parked.
25
26
  <section class="lares-agent__group">
26
27
  <h2>{{ tVoice("settings.title") }}</h2>
27
28
  <div class="lares-agent__list">
@@ -44,6 +45,7 @@
44
45
  </div>
45
46
  <p v-if="voiceError" class="lares-agent__hint" data-status="error">{{ voiceError }}</p>
46
47
  </section>
48
+ -->
47
49
  <section class="lares-agent__group">
48
50
  <h2>{{ tSearch("settings.title") }}</h2>
49
51
  <div class="lares-agent__list">
@@ -94,7 +96,9 @@ import {
94
96
  searchValueFromMenu,
95
97
  } from "@olares/lares-core/search/menu";
96
98
  import { rememberedSettings } from "@olares/lares-core/larepass/settings";
99
+ import { hostKey as hostSessionKey } from "@olares/lares-core/larepass/host";
97
100
  import { createHostClient } from "../host.js";
101
+ import { adoptHost } from "../runtime.js";
98
102
  import { createT } from "../i18n.js";
99
103
  import LaresSettingRow from "./SettingRow.vue";
100
104
  import LaresSheet from "./Sheet.vue";
@@ -138,13 +142,19 @@ export default {
138
142
  t() {
139
143
  return createT(this.locale);
140
144
  },
141
- settings() {
142
- return createHostClient({
145
+ ports() {
146
+ return {
143
147
  baseUrl: this.baseUrl,
144
148
  proxyPrefix: this.proxyPrefix,
145
149
  request: this.request,
146
150
  env: this.env,
147
- }).settings;
151
+ };
152
+ },
153
+ hostKey() {
154
+ return hostSessionKey(this.ports);
155
+ },
156
+ settings() {
157
+ return createHostClient(this.ports).settings;
148
158
  },
149
159
  busy() {
150
160
  return this.loading || Boolean(this.pending);
@@ -238,6 +248,19 @@ export default {
238
248
  return "";
239
249
  },
240
250
  },
251
+ watch: {
252
+ hostKey() {
253
+ adoptHost(this.ports);
254
+ this.panel = "";
255
+ this.models = null;
256
+ this.voice = null;
257
+ this.search = null;
258
+ this.modelError = "";
259
+ this.voiceError = "";
260
+ this.searchError = "";
261
+ this.load(true);
262
+ },
263
+ },
241
264
  mounted() {
242
265
  this.load(false);
243
266
  },
package/src/styles.css CHANGED
@@ -7,6 +7,21 @@
7
7
  color: var(--q-ink-1);
8
8
  }
9
9
 
10
+ .lares-shell__conversation {
11
+ display: flex;
12
+ flex-direction: column;
13
+ min-width: 0;
14
+ min-height: 0;
15
+ flex: 1;
16
+ width: 100%;
17
+ }
18
+
19
+ .lares-shell[data-device="desktop"] .lares-shell__conversation {
20
+ width: 748px;
21
+ max-width: 100%;
22
+ margin-inline: auto;
23
+ }
24
+
10
25
  .lares-shell button,
11
26
  .lares-shell textarea,
12
27
  .lares-shell select {