|
@@ -31,7 +31,7 @@ import TeamPresetPicker from '@/components/workspace/TeamPresetPicker.vue'
|
|
|
import { TEAM_PRESETS, type TeamPreset } from '@/components/groups/teamPresets'
|
|
import { TEAM_PRESETS, type TeamPreset } from '@/components/groups/teamPresets'
|
|
|
import type { WorkspaceMemberRole } from '@shared/workspace'
|
|
import type { WorkspaceMemberRole } from '@shared/workspace'
|
|
|
import AppIcon from '@/components/common/AppIcon.vue'
|
|
import AppIcon from '@/components/common/AppIcon.vue'
|
|
|
-import { workspaceAvatarMembers, workspaceAvatarState, workspaceConversationItem, workspaceMessagesToUi } from '@/components/workspace/viewModels'
|
|
|
|
|
|
|
+import { workspaceAgentActivity, workspaceAvatarMembers, workspaceAvatarState, workspaceConversationItem, workspaceMessagesToUi } from '@/components/workspace/viewModels'
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
import { useThemeStore } from '@/stores/theme'
|
|
import { useThemeStore } from '@/stores/theme'
|
|
|
import { apiRequest, ApiError } from '@/api/client'
|
|
import { apiRequest, ApiError } from '@/api/client'
|
|
@@ -74,8 +74,9 @@ const text = ref(''),
|
|
|
const identityBusy = ref(false),
|
|
const identityBusy = ref(false),
|
|
|
identityError = ref(''),
|
|
identityError = ref(''),
|
|
|
identityResetVersion = ref(0)
|
|
identityResetVersion = ref(0)
|
|
|
|
|
+const agentActivity = computed(() => workspaceAgentActivity(conversations.value))
|
|
|
const searchItems = computed<SidebarItem[]>(() => [false, true].map(archived => {
|
|
const searchItems = computed<SidebarItem[]>(() => [false, true].map(archived => {
|
|
|
- const items = conversations.value.filter(c => c.archived === archived).map(c => workspaceConversationItem(c, agents.value))
|
|
|
|
|
|
|
+ const items = conversations.value.filter(c => c.archived === archived).map(c => workspaceConversationItem(c, agents.value, agentActivity.value))
|
|
|
return { id: archived ? 'archived' : 'unarchived', title: archived ? '已归档' : '未归档', children: items, emptyText: archived ? '没有已归档聊天' : '没有未归档聊天' }
|
|
return { id: archived ? 'archived' : 'unarchived', title: archived ? '已归档' : '未归档', children: items, emptyText: archived ? '没有已归档聊天' : '没有未归档聊天' }
|
|
|
}))
|
|
}))
|
|
|
const archivedIds = computed(() => new Set(conversations.value.filter(c => c.archived).map(c => c.id)))
|
|
const archivedIds = computed(() => new Set(conversations.value.filter(c => c.archived).map(c => c.id)))
|
|
@@ -129,9 +130,11 @@ async function sendFromComposer(payload: ComposerSubmit) {
|
|
|
}
|
|
}
|
|
|
const dialog = ref<'agent' | 'group' | 'editAgent' | 'editGroup' | null>(null),
|
|
const dialog = ref<'agent' | 'group' | 'editAgent' | 'editGroup' | null>(null),
|
|
|
editingId = ref(''),
|
|
editingId = ref(''),
|
|
|
|
|
+ editingConversationId = ref(''),
|
|
|
scroller = ref<HTMLElement>(),
|
|
scroller = ref<HTMLElement>(),
|
|
|
fileInput = ref<HTMLInputElement>(),
|
|
fileInput = ref<HTMLInputElement>(),
|
|
|
dialogElement = ref<HTMLDialogElement>()
|
|
dialogElement = ref<HTMLDialogElement>()
|
|
|
|
|
+const editingConversation = computed(() => conversations.value.find(c => c.id === editingConversationId.value) ?? (active.value?.id === editingConversationId.value ? active.value : undefined))
|
|
|
const remotePickerOpen=ref(false)
|
|
const remotePickerOpen=ref(false)
|
|
|
const isRemoteAgent=computed(()=>dialog.value==='editAgent' && !!agents.value.find(a=>a.id===editingId.value)?.remoteAgentId)
|
|
const isRemoteAgent=computed(()=>dialog.value==='editAgent' && !!agents.value.find(a=>a.id===editingId.value)?.remoteAgentId)
|
|
|
async function remoteAdded(agent:Agent){
|
|
async function remoteAdded(agent:Agent){
|
|
@@ -391,10 +394,15 @@ async function select(id: string) {
|
|
|
pendingRequestId = undefined
|
|
pendingRequestId = undefined
|
|
|
await router.push(`/conversations/${id}`)
|
|
await router.push(`/conversations/${id}`)
|
|
|
}
|
|
}
|
|
|
-async function openDialog(kind: NonNullable<typeof dialog.value>) {
|
|
|
|
|
|
|
+function openConversationSettings(id: string) {
|
|
|
|
|
+ const conversation = conversations.value.find(c => c.id === id)
|
|
|
|
|
+ if (conversation) void openDialog(conversation.kind === 'direct' ? 'editAgent' : 'editGroup', conversation)
|
|
|
|
|
+}
|
|
|
|
|
+async function openDialog(kind: NonNullable<typeof dialog.value>, conversation = active.value) {
|
|
|
error.value = ''
|
|
error.value = ''
|
|
|
dialog.value = kind
|
|
dialog.value = kind
|
|
|
editingId.value = ''
|
|
editingId.value = ''
|
|
|
|
|
+ editingConversationId.value = kind === 'editAgent' || kind === 'editGroup' ? conversation?.id ?? '' : ''
|
|
|
selectedPresetId.value = 'custom'
|
|
selectedPresetId.value = 'custom'
|
|
|
Object.assign(form, {
|
|
Object.assign(form, {
|
|
|
name: '',
|
|
name: '',
|
|
@@ -417,8 +425,8 @@ async function openDialog(kind: NonNullable<typeof dialog.value>) {
|
|
|
form.source = s.sources[0] ? JSON.stringify([s.sources[0].nodeId, s.sources[0].profile]) : ''
|
|
form.source = s.sources[0] ? JSON.stringify([s.sources[0].nodeId, s.sources[0].profile]) : ''
|
|
|
}
|
|
}
|
|
|
if (kind === 'editAgent') {
|
|
if (kind === 'editAgent') {
|
|
|
- const a = members.value[0]
|
|
|
|
|
- if (!a) return
|
|
|
|
|
|
|
+ const a = agents.value.find(a => a.id === conversation?.memberIds[0])
|
|
|
|
|
+ if (!a) throw new Error('机器人不存在,请刷新后重试')
|
|
|
editingId.value = a.id
|
|
editingId.value = a.id
|
|
|
Object.assign(form, a)
|
|
Object.assign(form, a)
|
|
|
form.source=JSON.stringify([a.nodeId,a.profile])
|
|
form.source=JSON.stringify([a.nodeId,a.profile])
|
|
@@ -427,12 +435,12 @@ async function openDialog(kind: NonNullable<typeof dialog.value>) {
|
|
|
if(remote)Object.assign(form,remote)
|
|
if(remote)Object.assign(form,remote)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- if (kind === 'editGroup' && active.value) {
|
|
|
|
|
- editingId.value = active.value.id
|
|
|
|
|
- Object.assign(form, active.value, {
|
|
|
|
|
- memberIds: [...active.value.memberIds],
|
|
|
|
|
- autoReplyIds: [...active.value.autoReplyIds],
|
|
|
|
|
- memberRoles: Object.fromEntries(Object.entries(active.value.memberRoles ?? {}).map(([id, role]) => [id, { ...role }])),
|
|
|
|
|
|
|
+ if (kind === 'editGroup' && conversation) {
|
|
|
|
|
+ editingId.value = conversation.id
|
|
|
|
|
+ Object.assign(form, conversation, {
|
|
|
|
|
+ memberIds: [...conversation.memberIds],
|
|
|
|
|
+ autoReplyIds: [...conversation.autoReplyIds],
|
|
|
|
|
+ memberRoles: Object.fromEntries(Object.entries(conversation.memberRoles ?? {}).map(([id, role]) => [id, { ...role }])),
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
await nextTick()
|
|
await nextTick()
|
|
@@ -445,6 +453,7 @@ async function openDialog(kind: NonNullable<typeof dialog.value>) {
|
|
|
function closeDialog() {
|
|
function closeDialog() {
|
|
|
dialogElement.value?.close()
|
|
dialogElement.value?.close()
|
|
|
dialog.value = null
|
|
dialog.value = null
|
|
|
|
|
+ editingConversationId.value = ''
|
|
|
}
|
|
}
|
|
|
watch(
|
|
watch(
|
|
|
() => form.memberIds.slice(),
|
|
() => form.memberIds.slice(),
|
|
@@ -611,9 +620,9 @@ async function changeLifecycle(operation: WorkspaceLifecycleAction, id: string,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
async function stopMember(id: string) {
|
|
async function stopMember(id: string) {
|
|
|
- if (!active.value) return
|
|
|
|
|
|
|
+ if (!editingConversation.value) return
|
|
|
try {
|
|
try {
|
|
|
- await apiRequest(`/api/app/conversations/${active.value.id}/agents/${id}/stop`, { method: 'POST', body: {} })
|
|
|
|
|
|
|
+ await apiRequest(`/api/app/conversations/${editingConversation.value.id}/agents/${id}/stop`, { method: 'POST', body: {} })
|
|
|
await load(); await refresh()
|
|
await load(); await refresh()
|
|
|
} catch (cause) { error.value = cause instanceof Error ? cause.message : '停止失败' }
|
|
} catch (cause) { error.value = cause instanceof Error ? cause.message : '停止失败' }
|
|
|
}
|
|
}
|
|
@@ -714,10 +723,10 @@ onBeforeUnmount(() => {
|
|
|
@create-remote-agent="remotePickerOpen = true"
|
|
@create-remote-agent="remotePickerOpen = true"
|
|
|
>
|
|
>
|
|
|
<template #sidebar
|
|
<template #sidebar
|
|
|
- ><ConversationList :conversations="conversations" :agents="agents" :selected="selected" @select="select" @pin="action('pin', $event)" @archive="action('archive', $event)" @delete="changeLifecycle('delete', $event)"
|
|
|
|
|
|
|
+ ><ConversationList :conversations="conversations" :agents="agents" :selected="selected" @select="select" @settings="openConversationSettings" @pin="action('pin', $event)" @archive="action('archive', $event)" @delete="changeLifecycle('delete', $event)"
|
|
|
/></template>
|
|
/></template>
|
|
|
<template #mobile-sidebar
|
|
<template #mobile-sidebar
|
|
|
- ><ConversationList :conversations="conversations" :agents="agents" :selected="selected" @select="select" @pin="action('pin', $event)" @archive="action('archive', $event)" @delete="changeLifecycle('delete', $event)"
|
|
|
|
|
|
|
+ ><ConversationList :conversations="conversations" :agents="agents" :selected="selected" @select="select" @settings="openConversationSettings" @pin="action('pin', $event)" @archive="action('archive', $event)" @delete="changeLifecycle('delete', $event)"
|
|
|
/></template>
|
|
/></template>
|
|
|
<div v-show="!twoDesktops" class="conversation-with-computer">
|
|
<div v-show="!twoDesktops" class="conversation-with-computer">
|
|
|
<section class="workspace-chat" aria-label="聊天" @click.capture="openTaskLink">
|
|
<section class="workspace-chat" aria-label="聊天" @click.capture="openTaskLink">
|
|
@@ -778,11 +787,11 @@ onBeforeUnmount(() => {
|
|
|
<PreviewModal v-if="preview" :item="preview" :items="media" @close="preview = null" />
|
|
<PreviewModal v-if="preview" :item="preview" :items="media" @close="preview = null" />
|
|
|
<ImagePreviewLightbox v-model="mediaIndex" :images="lightboxMedia" />
|
|
<ImagePreviewLightbox v-model="mediaIndex" :images="lightboxMedia" />
|
|
|
<RemoteAgentPicker v-if="remotePickerOpen && !auth.isBotOnly" @close="remotePickerOpen = false" @added="remoteAdded" />
|
|
<RemoteAgentPicker v-if="remotePickerOpen && !auth.isBotOnly" @close="remotePickerOpen = false" @added="remoteAdded" />
|
|
|
- <Teleport to="body"><dialog v-if="dialog" ref="dialogElement" class="editor" @cancel.prevent="closeDialog">
|
|
|
|
|
|
|
+ <Teleport to="body"><dialog v-if="dialog" ref="dialogElement" class="editor" aria-labelledby="conversation-editor-title" @cancel.prevent="closeDialog">
|
|
|
<form @submit.prevent="save">
|
|
<form @submit.prevent="save">
|
|
|
<header>
|
|
<header>
|
|
|
- <h2>
|
|
|
|
|
- {{ dialog === 'agent' ? '创建机器人' : dialog === 'group' ? '创建群聊' : '编辑资料' }}
|
|
|
|
|
|
|
+ <h2 id="conversation-editor-title">
|
|
|
|
|
+ {{ dialog === 'agent' ? '创建机器人' : dialog === 'group' ? '创建群聊' : dialog === 'editAgent' ? '机器人设置' : '群聊设置' }}
|
|
|
</h2>
|
|
</h2>
|
|
|
<button type="button" aria-label="关闭" @click="closeDialog">×</button>
|
|
<button type="button" aria-label="关闭" @click="closeDialog">×</button>
|
|
|
</header>
|
|
</header>
|
|
@@ -792,7 +801,7 @@ onBeforeUnmount(() => {
|
|
|
<p v-if="isRemoteAgent">引用的机器人配置由远端管理,请在远端修改名称、头像和角色规则。</p>
|
|
<p v-if="isRemoteAgent">引用的机器人配置由远端管理,请在远端修改名称、头像和角色规则。</p>
|
|
|
<AgentIdentityPanel v-if="isAgentDialog && !isRemoteAgent" :key="`${dialog}:${editingId}`" :profile="avatarProfile" embedded :show-name="false" :show-default-model="false" :show-actions="false" @avatar-change="form.avatar = $event" />
|
|
<AgentIdentityPanel v-if="isAgentDialog && !isRemoteAgent" :key="`${dialog}:${editingId}`" :profile="avatarProfile" embedded :show-name="false" :show-default-model="false" :show-actions="false" @avatar-change="form.avatar = $event" />
|
|
|
<div v-else-if="!isAgentDialog" class="team-avatar-settings">
|
|
<div v-else-if="!isAgentDialog" class="team-avatar-settings">
|
|
|
- <TeamAvatar :name="form.name" :members="workspaceAvatarMembers(form.memberIds, agents, dialog === 'editGroup' ? active : undefined)" :size="64" />
|
|
|
|
|
|
|
+ <TeamAvatar :name="form.name" :members="workspaceAvatarMembers(form.memberIds, agents, dialog === 'editGroup' ? editingConversation : undefined)" :size="64" />
|
|
|
<small>群聊头像由成员头像自动组合,随成员头像更新。</small>
|
|
<small>群聊头像由成员头像自动组合,随成员头像更新。</small>
|
|
|
</div>
|
|
</div>
|
|
|
<label
|
|
<label
|
|
@@ -842,20 +851,20 @@ onBeforeUnmount(() => {
|
|
|
<legend>选择成员</legend>
|
|
<legend>选择成员</legend>
|
|
|
<label
|
|
<label
|
|
|
v-for="a in agents.filter((a) =>
|
|
v-for="a in agents.filter((a) =>
|
|
|
- !a.temporaryGoalId && (!a.archived || (dialog === 'editGroup' && active?.memberIds.includes(a.id))),
|
|
|
|
|
|
|
+ !a.temporaryGoalId && (!a.archived || (dialog === 'editGroup' && editingConversation?.memberIds.includes(a.id))),
|
|
|
)"
|
|
)"
|
|
|
:key="a.id"
|
|
:key="a.id"
|
|
|
><input
|
|
><input
|
|
|
v-model="form.memberIds"
|
|
v-model="form.memberIds"
|
|
|
type="checkbox"
|
|
type="checkbox"
|
|
|
:value="a.id"
|
|
:value="a.id"
|
|
|
- :disabled="busy || (dialog === 'editGroup' && (a.id === active?.administratorId || a.id === form.administratorId)) || (!form.memberIds.includes(a.id) && form.memberIds.length >= 8)"
|
|
|
|
|
|
|
+ :disabled="busy || (dialog === 'editGroup' && (a.id === editingConversation?.administratorId || a.id === form.administratorId)) || (!form.memberIds.includes(a.id) && form.memberIds.length >= 8)"
|
|
|
/>{{ a.name }}</label
|
|
/>{{ a.name }}</label
|
|
|
><small v-if="dialog === 'group' && agents.filter((a) => !a.archived && !a.temporaryGoalId).length < 2"
|
|
><small v-if="dialog === 'group' && agents.filter((a) => !a.archived && !a.temporaryGoalId).length < 2"
|
|
|
>至少需要两个机器人才能创建群聊。</small
|
|
>至少需要两个机器人才能创建群聊。</small
|
|
|
>
|
|
>
|
|
|
<small v-if="dialog === 'editGroup'">可增减成员,最多 8 位。当前管理员不能移除;如需移除,请先更换管理员并保存。</small>
|
|
<small v-if="dialog === 'editGroup'">可增减成员,最多 8 位。当前管理员不能移除;如需移除,请先更换管理员并保存。</small>
|
|
|
- <button v-for="a in agents.filter(a => active?.activeAgentStates?.[a.id])" :key="`stop:${a.id}`" type="button" @click="stopMember(a.id)">停止 {{ a.name }}</button>
|
|
|
|
|
|
|
+ <button v-for="a in agents.filter(a => editingConversation?.activeAgentStates?.[a.id])" :key="`stop:${a.id}`" type="button" @click="stopMember(a.id)">停止 {{ a.name }}</button>
|
|
|
</fieldset>
|
|
</fieldset>
|
|
|
<fieldset v-if="dialog === 'editGroup' && Object.keys(form.memberRoles).length">
|
|
<fieldset v-if="dialog === 'editGroup' && Object.keys(form.memberRoles).length">
|
|
|
<legend>角色分工</legend>
|
|
<legend>角色分工</legend>
|