Переглянути джерело

feat: Android App 会话中可切换模型(0.1.1)

输入框工具栏加入模型选择,可选模型来自 /v1/providers,运行中不可切换。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang 6 днів тому
батько
коміт
6fd0b7e93d
2 змінених файлів з 60 додано та 3 видалено
  1. 1 1
      apps/mobile/package.json
  2. 59 2
      apps/mobile/src/views/ChatView.vue

+ 1 - 1
apps/mobile/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@agent-server/mobile",
-  "version": "0.1.0",
+  "version": "0.1.1",
   "private": true,
   "type": "module",
   "scripts": {

+ 59 - 2
apps/mobile/src/views/ChatView.vue

@@ -2,7 +2,7 @@
 // 单个会话:<AgentChat>(流式输出、审批与提问、上传、定时任务、后台调用都由组件处理)
 import { computed, onBeforeUnmount, ref, watch } from 'vue'
 import { useRouter } from 'vue-router'
-import type { SessionDetailDto } from '@agent-server/client'
+import type { ProviderStatusDto, SessionDetailDto } from '@agent-server/client'
 import { AgentChat, useChatSession } from '@agent-server/chat-ui'
 import TopBar from '../components/TopBar.vue'
 import { agentClient } from '../lib/config'
@@ -24,6 +24,8 @@ async function refresh() {
   }
 }
 void refresh()
+const providers = ref<ProviderStatusDto[]>([])
+void client.providers().then(p => { providers.value = p }).catch(() => {})
 // 一轮结束后重新读取标题(首轮结束时服务端会自动起标题)
 watch(() => chat.state.runState, s => { if (s === 'idle') void refresh() })
 onBeforeUnmount(() => chat.dispose())
@@ -36,6 +38,25 @@ const subtitle = computed(() => {
   return parts.filter(Boolean).join(' · ')
 })
 
+// 可选模型来自引擎设置;会话当前的模型不在列表中时(例如 Profile 指定的模型)也保留
+const provider = computed(() => providers.value.find(p => p.id === detail.value?.provider))
+const models = computed(() => {
+  const list = provider.value?.models ?? []
+  const cur = detail.value?.model
+  return cur && !list.includes(cur) ? [cur, ...list] : list
+})
+
+async function setModel(model: string) {
+  if (!detail.value || model === detail.value.model) return
+  try {
+    await client.sessions.update(props.id, { model })
+    await refresh()
+  } catch (err) {
+    toast((err as Error).message, 'error')
+    await refresh()
+  }
+}
+
 async function menu() {
   if (!detail.value) return
   const r = await sessionMenu(client, detail.value, [{ value: 'files', label: '文件', icon: 'files' }])
@@ -51,6 +72,42 @@ async function menu() {
     <button type="button" class="m-icon" aria-label="更多" @click="menu"><span class="codicon codicon-ellipsis text-[18px]" /></button>
   </TopBar>
   <div class="flex-1 min-h-0 pb-[var(--safe-bottom)] bg-[var(--app-primary-background)]">
-    <AgentChat :client="client" :session="chat" placeholder="问点什么,或上传文件…" @open-file="f => openFile(client, f)" />
+    <AgentChat :client="client" :session="chat" placeholder="问点什么,或上传文件…" @open-file="f => openFile(client, f)">
+      <template #toolbar>
+        <label v-if="detail && models.length" class="model-pill">
+          <span class="codicon codicon-sparkle text-[12px]" />
+          <select :value="detail.model" :disabled="chat.state.runState !== 'idle'" aria-label="模型" @change="setModel(($event.target as HTMLSelectElement).value)">
+            <option v-for="m in models" :key="m" :value="m">{{ m }}{{ provider?.modelLabels?.[m] ? ` · ${provider.modelLabels[m]}` : '' }}</option>
+          </select>
+        </label>
+      </template>
+    </AgentChat>
   </div>
 </template>
+
+<style scoped>
+.model-pill {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  min-width: 0;
+  height: 26px;
+  padding: 0 8px;
+  border-radius: 13px;
+  border: 1px solid var(--app-primary-border-color);
+  color: var(--app-secondary-foreground);
+  font-size: 12px;
+}
+.model-pill select {
+  border: none;
+  background: transparent;
+  color: inherit;
+  outline: none;
+  padding: 0;
+  font-size: 12px;
+  max-width: 150px;
+  text-overflow: ellipsis;
+}
+.model-pill select option { background: var(--app-input-secondary-background); color: var(--app-primary-foreground); }
+.model-pill select:disabled { opacity: 0.6; }
+</style>