浏览代码

统一团队相关产品文案

LSamien 2 月之前
父节点
当前提交
995dc3a97b

+ 1 - 1
index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
     <meta name="theme-color" content="#fafafa" />
-    <meta name="description" content="夭夭 Web:Hermes 聊天、群聊、文件库与产物工作台" />
+    <meta name="description" content="夭夭 Web:Hermes 聊天、团队、文件库与产物工作台" />
     <link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
     <link rel="manifest" href="/manifest.webmanifest" />

+ 1 - 1
package.json

@@ -3,7 +3,7 @@
   "version": "0.1.1",
   "private": true,
   "license": "BSL-1.1",
-  "description": "夭夭 Web:直连 Hermes 9119 的聊天、群聊、文件库与产物工作台",
+  "description": "夭夭 Web:直连 Hermes 9119 的聊天、团队、文件库与产物工作台",
   "type": "module",
   "engines": {
     "node": ">=24.0.0"

+ 1 - 1
src/client/App.vue

@@ -16,7 +16,7 @@ const route = useRoute()
 
 const pageTitle = computed(() => {
   const name = route.path.startsWith('/groups')
-    ? groups.selectedRoom?.name || '群聊'
+    ? groups.selectedRoom?.name || '团队'
     : route.path.startsWith('/files')
       ? '文件库'
       : chat.activeSession?.title || '对话'

+ 1 - 1
src/client/api/realtime.ts

@@ -237,7 +237,7 @@ export class GroupEventSocket {
       socket.addEventListener('error', () => {
         if (!settled) {
           settled = true
-          reject(new ApiError('群聊事件连接失败', 0, 'GROUP_WEBSOCKET_ERROR'))
+          reject(new ApiError('团队事件连接失败', 0, 'GROUP_WEBSOCKET_ERROR'))
         }
       })
       socket.addEventListener('close', event => {

+ 2 - 2
src/client/components/app/NodePairingDialog.vue

@@ -138,7 +138,7 @@ onBeforeUnmount(stopPolling)
             <button class="icon-button" type="button" aria-label="关闭手机与节点" @click="emit('close')"><AppIcon name="close" /></button>
           </header>
 
-          <p class="pairing-intro">扫描一次即可在 iOS 中使用这台 Hermes 的 Bots、聊天历史和原生群聊 Agent。</p>
+          <p class="pairing-intro">扫描一次即可在 iOS 中使用这台 Hermes 的 Bots、聊天历史和团队 Agent。</p>
           <p v-if="insecureTransport" class="pairing-warning"><AppIcon name="alert" :size="15" />当前是局域网 HTTP,二维码免密配对仅适用于可信网络。正式使用请配置 HTTPS/WSS 或 Tailscale。</p>
           <p v-if="error" class="pairing-error">{{ error }}</p>
 
@@ -163,7 +163,7 @@ onBeforeUnmount(stopPolling)
             <div class="device-heading"><strong>已授权设备</strong><small v-if="nodeID">节点 {{ nodeID.slice(0, 8) }}</small></div>
             <p v-if="!busy && !devices.length" class="device-empty">还没有已授权设备。</p>
             <div v-for="device in devices" :key="device.id" class="device-row">
-              <span><b>{{ device.name }}</b><small>{{ device.scopes.includes('history.read') ? 'Bots、历史与群聊' : '受限访问' }}</small></span>
+              <span><b>{{ device.name }}</b><small>{{ device.scopes.includes('history.read') ? 'Bots、历史与团队' : '受限访问' }}</small></span>
               <button type="button" :disabled="busy" @click="revoke(device)">撤销</button>
             </div>
           </section>

+ 2 - 2
src/client/components/app/WorkspaceShell.vue

@@ -71,7 +71,7 @@ function profileTitle(profile?: Profile): string {
 
 const navItems: NavItem[] = [
   { key: 'chat', label: '对话', path: '/chat', icon: 'chat' },
-  { key: 'groups', label: '群聊', path: '/groups', icon: 'groups' },
+  { key: 'groups', label: '团队', path: '/groups', icon: 'groups' },
   { key: 'files', label: '文件库', path: '/files', icon: 'files' },
 ]
 
@@ -87,7 +87,7 @@ const activeNav = computed(() => {
 
 const contextHeading = computed(() => ({
   chat: '历史记录',
-  groups: '群聊记录',
+  groups: '团队列表',
   files: '历史记录',
 })[activeNav.value.key])
 

+ 1 - 1
src/client/components/composer/ComposerShell.vue

@@ -137,7 +137,7 @@ function isImage(type: string) { return type.startsWith('image/') }
 
 function addFiles(files: File[]) {
   if (!props.attachmentsEnabled) {
-    showError('当前上游不支持群聊附件')
+    showError('当前上游不支持团队附件')
     return
   }
   const available = MAX_FILES - attachments.value.length

+ 3 - 3
src/client/components/groups/CreateGroupDialog.vue

@@ -71,8 +71,8 @@ function create() {
     <Transition name="dialog-fade">
       <div v-if="open" class="dialog-layer" role="presentation" @mousedown.self="emit('close')">
         <section class="create-dialog" role="dialog" aria-modal="true" aria-labelledby="create-group-title">
-          <header><div><small>9119 群聊</small><h2 id="create-group-title">新建群聊</h2></div><button class="icon-button" type="button" aria-label="关闭" @click="emit('close')"><AppIcon name="close" /></button></header>
-          <label class="field"><span>群聊名称</span><input v-model="name" maxlength="80" autofocus placeholder="例如:产品评审" /></label>
+          <header><div><small>9119 团队</small><h2 id="create-group-title">新建团队</h2></div><button class="icon-button" type="button" aria-label="关闭" @click="emit('close')"><AppIcon name="close" /></button></header>
+          <label class="field"><span>团队名称</span><input v-model="name" maxlength="80" autofocus placeholder="例如:产品评审" /></label>
           <label v-if="roomInstructionsEnabled" class="field room-instructions"><span>说明 <small>供所有 Agent 查阅的规则和形式准则</small></span><textarea v-model="instructions" maxlength="4000" rows="4" placeholder="例如:先确认事实;结论使用中文;涉及发布必须等待确认。" /></label>
           <div class="agent-picker">
             <div class="agent-picker__heading"><span>选择 Agent</span><small>{{ selected.length }}/8</small></div>
@@ -98,7 +98,7 @@ function create() {
             <label><input v-model="autoReply" type="checkbox" :aria-label="hostEnabled ? '所有成员无需 @ 也回复' : '启用自动回复'" />{{ hostEnabled ? (orchestrationMode === 'host' ? '自由讨论时无需 @ 也回复' : '所有成员无需 @ 也回复') : '启用自动回复' }}</label>
             <label>最多轮数 <input v-model.number="replyRounds" type="number" min="1" max="12" /></label>
           </div>
-          <footer><button class="quiet-button" type="button" @click="emit('close')">取消</button><button class="solid-button" type="button" :disabled="!valid || busy" @click="create">{{ busy ? '创建中…' : '创建群聊' }}</button></footer>
+          <footer><button class="quiet-button" type="button" @click="emit('close')">取消</button><button class="solid-button" type="button" :disabled="!valid || busy" @click="create">{{ busy ? '创建中…' : '创建团队' }}</button></footer>
         </section>
       </div>
     </Transition>

+ 5 - 5
src/client/components/groups/GroupManager.vue

@@ -147,7 +147,7 @@ function canRemoveAgent(agent: GroupAgent): boolean {
 function isRemoteAgent(agent: GroupAgent): boolean { return agent.nodeId !== 'local' }
 
 function removeAgentTitle(agent: GroupAgent): string {
-  return canRemoveAgent(agent) ? '移除' : agent.isHost ? '需要另一位已启用成员才能移除主持人' : '房间必须保留至少一位成员'
+  return canRemoveAgent(agent) ? '移除' : agent.isHost ? '需要另一位已启用成员才能移除主持人' : '团队必须保留至少一位成员'
 }
 
 function openAgentSettings(agent: GroupAgent) {
@@ -218,13 +218,13 @@ function statusLabel(status: GroupAgent['status']): string {
 <template>
   <div class="group-manager">
     <header>
-      <span><small>群聊管理</small><strong>{{ room.name }}</strong></span>
+      <span><small>团队管理</small><strong>{{ room.name }}</strong></span>
     </header>
 
     <section>
-      <h3>房间</h3>
+      <h3>团队设置</h3>
       <label><span>名称</span><input v-model="form.name" maxlength="80" @change="saveRoom" /></label>
-      <label v-if="roomInstructionsEnabled"><span>说明<small>所有 Agent 都会在回复前查阅,可填写协作规则和形式准则。</small></span><textarea v-model="form.instructions" maxlength="4000" rows="5" aria-label="房间说明" placeholder="例如:先核对事实;结论使用中文;发布前等待确认。" @change="saveRoom" /></label>
+      <label v-if="roomInstructionsEnabled"><span>说明<small>所有 Agent 都会在回复前查阅,可填写协作规则和形式准则。</small></span><textarea v-model="form.instructions" maxlength="4000" rows="5" aria-label="团队说明" placeholder="例如:先核对事实;结论使用中文;发布前等待确认。" @change="saveRoom" /></label>
       <label class="rounds"><span>最多回复轮数<small>-1 表示无限</small></span><input v-model.number="form.replyRounds" type="number" min="-1" max="100" aria-label="最多回复轮数" @change="saveRoom" /></label>
       <label v-if="hostFlowEnabled" class="flow-mode">
         <span>协作模式<small>主持人可按依赖逐步调度,也可一次 @ 多人并列执行。</small></span>
@@ -263,7 +263,7 @@ function statusLabel(status: GroupAgent['status']): string {
     </section>
 
     <section class="danger-zone">
-      <div><strong>归档群聊</strong><p>归档后不再出现在活跃列表,历史消息仍会保留。</p></div>
+      <div><strong>归档团队</strong><p>归档后不再出现在活跃列表,历史消息仍会保留。</p></div>
       <button class="quiet-button" type="button" :disabled="busy" @click="emit('archiveRoom')"><AppIcon name="archive" :size="14" />归档</button>
     </section>
   </div>

+ 1 - 1
src/client/components/library/ImagePreviewLightbox.vue

@@ -62,7 +62,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
     >
       <div class="image-preview-actions">
         <a :href="previewMedia.url" download :aria-label="`下载 ${previewMedia.name}`" :title="`下载 ${previewMedia.name}`"><DownloadOutline aria-hidden="true" /></a>
-        <button type="button" :disabled="canAdd === false" :aria-label="addLabel || '添加到聊天'" :title="canAdd === false ? '当前群聊不支持添加附件' : addLabel || '添加到聊天'" @click.stop="emit('add', previewMedia)"><AddOutline aria-hidden="true" /></button>
+        <button type="button" :disabled="canAdd === false" :aria-label="addLabel || '添加到聊天'" :title="canAdd === false ? '当前团队不支持添加附件' : addLabel || '添加到聊天'" @click.stop="emit('add', previewMedia)"><AddOutline aria-hidden="true" /></button>
         <button type="button" :aria-label="closeLabel || '关闭预览'" :title="closeLabel || '关闭预览'" @click="close"><CloseOutline aria-hidden="true" /></button>
       </div>
       <button v-if="canPrevious" class="image-preview-nav image-preview-nav--previous" type="button" :aria-label="previousLabel || '上一张媒体'" @click.stop="move(-1)"><ChevronBackOutline aria-hidden="true" /></button>

+ 1 - 1
src/client/components/workspace/viewModels.ts

@@ -186,7 +186,7 @@ export function chatInteraction(approval?: ApprovalRequest, clarification?: Clar
 export function roomSidebarItem(room: GroupRoomSummary): SidebarItem {
   return {
     id: room.id,
-    title: room.name || '未命名群聊',
+    title: room.name || '未命名团队',
     subtitle: room.lastMessage?.content || `${room.agentCount} 个 Agent`,
     meta: formatRelative(room.updatedAt),
     section: historySection(room.updatedAt),

+ 7 - 7
src/client/stores/groups.ts

@@ -29,7 +29,7 @@ function initialProtocol(): GroupProtocolState {
   return { epoch: '', cursor: 0, rooms: [], roomDetails: {}, messagesByRoom: {}, topicsByRoom: {}, messagesByTopic: {} }
 }
 
-function errorMessage(cause: unknown): string { return cause instanceof Error ? cause.message : '群聊请求失败' }
+function errorMessage(cause: unknown): string { return cause instanceof Error ? cause.message : '团队请求失败' }
 
 function topicTitle(content: string): string {
   return content.split(/\s+/).filter(Boolean).join(' ').slice(0, 100) || '新话题'
@@ -89,7 +89,7 @@ export const useGroupsStore = defineStore('groups', () => {
     let cursor: string | undefined
     do {
       const key = cursor ?? '<first-page>'
-      if (seen.has(key)) throw new Error('群聊房间分页返回了重复游标')
+      if (seen.has(key)) throw new Error('团队列表分页返回了重复游标')
       seen.add(key)
       const page = await getGroupRooms(cursor, 50)
       loaded.push(...page.items)
@@ -104,7 +104,7 @@ export const useGroupsStore = defineStore('groups', () => {
     let cursor: string | undefined
     do {
       const key = cursor ?? '<first-page>'
-      if (seen.has(key)) throw new Error('群聊话题分页返回了重复游标')
+      if (seen.has(key)) throw new Error('团队话题分页返回了重复游标')
       seen.add(key)
       const page = await getGroupTopics(roomId, cursor, 50, archived)
       loaded.push(...page.items)
@@ -170,7 +170,7 @@ export const useGroupsStore = defineStore('groups', () => {
       },
       onReset(reason) {
         if (expectedGeneration !== generation) return
-        error.value = `群聊事件需要重新同步:${reason}`
+        error.value = `团队事件需要重新同步:${reason}`
         void rebuildSnapshot()
       },
     })
@@ -220,7 +220,7 @@ export const useGroupsStore = defineStore('groups', () => {
         const anchor = await getGroupCapabilities()
         if (!isSupportedGroupProtocolVersion(anchor.protocolVersion)) {
           availability.value = 'unsupported'
-          throw new Error(`群聊协议版本不兼容:支持 ${SUPPORTED_GROUP_PROTOCOL_VERSION_LABEL},服务器返回 v${anchor.protocolVersion || 'unknown'}`)
+          throw new Error(`团队协议版本不兼容:支持 ${SUPPORTED_GROUP_PROTOCOL_VERSION_LABEL},服务器返回 v${anchor.protocolVersion || 'unknown'}`)
         }
         const loadedRooms = await loadRooms()
         nodes.value = anchor.features?.includes('nodeRegistry')
@@ -609,7 +609,7 @@ export const useGroupsStore = defineStore('groups', () => {
     let uploadIds: string[] = []
     let optimisticText = text
     if (files.length) {
-      if (!auth.groupUploadsEnabled) throw new Error('当前上游不支持群聊附件')
+      if (!auth.groupUploadsEnabled) throw new Error('当前上游不支持团队附件')
       const uploaded = await uploadGroupFiles(roomId, files)
       uploadIds = uploaded.map(file => file.id)
       optimisticText = [text, ...uploaded.map(file => `📎 ${file.name}`)].filter(Boolean).join('\n\n')
@@ -695,7 +695,7 @@ export const useGroupsStore = defineStore('groups', () => {
       const failed = {
         ...optimistic,
         status: explicitRejection ? 'failed' as const : 'unknown' as const,
-        error: explicitRejection ? errorMessage(cause) : '送达状态未知;刷新房间后再决定是否重试',
+        error: explicitRejection ? errorMessage(cause) : '送达状态未知;刷新团队后再决定是否重试',
         updatedAt: Date.now() / 1000,
       }
       protocol.value = topicId ? {

+ 1 - 1
src/client/utils/normalize.ts

@@ -443,7 +443,7 @@ export function normalizeGroupRoom(value: unknown): GroupRoomSummary {
   const rawAgents = source.agents
   const orchestrationMode = string(pick(source, 'orchestrationMode', 'orchestration_mode'))
   return {
-    id: string(source.id), name: string(source.name, '未命名群聊'), cwd: string(source.cwd), instructions: string(source.instructions),
+    id: string(source.id), name: string(source.name, '未命名团队'), cwd: string(source.cwd), instructions: string(source.instructions),
     createdAt: number(pick(source, 'createdAt', 'created_at')), updatedAt: number(pick(source, 'updatedAt', 'updated_at')),
     archived: bool(source.archived), agentCount: Array.isArray(rawAgents) ? rawAgents.length : number(pick(source, 'agentCount', 'agent_count')),
     lastMessage: source.lastMessage || source.last_message ? normalizeGroupMessage(pick(source, 'lastMessage', 'last_message')) : null,

+ 22 - 22
src/client/views/GroupsView.vue

@@ -61,7 +61,7 @@ const pinnedTopics = computed(() => groups.pinnedTopics.filter(topic => !topic.a
 const topicListRoom = computed(() => topicListRoomId.value ? activeRooms.value.find(room => room.id === topicListRoomId.value) : undefined)
 const sidebarSubtitle = computed(() => topicListRoom.value
   ? `${topicListRoom.value.agentCount} 个 Agent`
-  : groups.availability === 'available' ? `${activeRooms.value.length} 个活跃房间` : `9119 群聊 ${SUPPORTED_GROUP_PROTOCOL_VERSION_LABEL}`)
+  : groups.availability === 'available' ? `${activeRooms.value.length} 个活跃团队` : `9119 团队 ${SUPPORTED_GROUP_PROTOCOL_VERSION_LABEL}`)
 const GROUP_LIST_BACK_ID = 'group-list'
 const NEW_TOPIC_ID = 'new-topic'
 function topicSidebarItemId(roomId: string, topicId: string): string { return `topic:${roomId}:${topicId}` }
@@ -84,7 +84,7 @@ const sidebarItems = computed<SidebarItem[]>(() => {
   if (!room) return activeRooms.value.map(item => ({ ...roomSidebarItem(item), section: undefined }))
   const items: SidebarItem[] = [{
     id: GROUP_LIST_BACK_ID,
-    title: '返回房间列表',
+    title: '返回团队列表',
     icon: 'chevron-left',
     showMore: false,
   }, {
@@ -183,7 +183,7 @@ const managerBusy = computed(() => groups.isLoading || Object.values(agentUpdate
 const reference = computed<ComposerReference | null>(() => quoted.value ? { id: quoted.value.id, author: quoted.value.author, content: quoted.value.content } : null)
 const mentionNames = computed(() => ['所有人', ...displayAgents.value.map(agent => agent.displayName || agent.profile)])
 const mentionOptions = computed<ComposerOption[]>(() => [
-  { id: 'all', label: '所有人', detail: '通知房间内全部 Agent' },
+  { id: 'all', label: '所有人', detail: '通知团队内全部 Agent' },
   ...displayAgents.value.map(agent => ({
     id: agent.id,
     label: agent.displayName || agent.profile,
@@ -192,7 +192,7 @@ const mentionOptions = computed<ComposerOption[]>(() => [
   })),
 ])
 const roomSubtitle = computed(() => {
-  if (!groups.selectedRoom) return '选择或新建一个群聊'
+  if (!groups.selectedRoom) return '选择或新建一个团队'
   const host = hostAgent.value ? `主持人 ${hostAgent.value.displayName || hostAgent.value.profile} · ` : ''
   const mode = groups.selectedRoom.orchestrationMode === 'host' ? '主持流程 · ' : ''
   return `${groups.selectedRoom.name} · ${mode}${host}${groups.agents.length} 个 Agent · 最多 ${groups.selectedRoom.maxReplyRounds} 轮回复`
@@ -611,11 +611,11 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
 </script>
 
 <template>
-  <WorkspaceView sidebar-title="群聊" :sidebar-context-title="topicListRoom?.name" :sidebar-subtitle="sidebarSubtitle" :sidebar-focus-mode="!!topicListRoom" :inspector-open="managerOpen && !!room" inspector-close-label="关闭群聊管理" @close-inspector="managerOpen = false">
+  <WorkspaceView sidebar-title="团队" :sidebar-context-title="topicListRoom?.name" :sidebar-subtitle="sidebarSubtitle" :sidebar-focus-mode="!!topicListRoom" :inspector-open="managerOpen && !!room" inspector-close-label="关闭团队管理" @close-inspector="managerOpen = false">
     <template #sidebar-action>
-      <button class="sidebar-primary-action" type="button" :disabled="groups.availability !== 'available'" title="新建群聊" aria-label="新建群聊" @click="openCreateGroup">
+      <button class="sidebar-primary-action" type="button" :disabled="groups.availability !== 'available'" title="新建团队" aria-label="新建团队" @click="openCreateGroup">
         <YaoYaoSidebarIcon name="add" />
-        <span>新建群聊</span>
+        <span>新建团队</span>
       </button>
       <button class="sidebar-primary-action" type="button" :disabled="groups.availability !== 'available'" @click="openArchivedOverlay">
         <AppIcon name="archive" :size="15" />
@@ -639,9 +639,9 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
         :active-id="groups.selectedRoomId"
         :loading="groups.isLoading"
         external-search
-        search-placeholder="搜索群聊"
-        empty-title="还没有群聊"
-        empty-description="新建群聊,邀请 1–8 个 Agent 一起协作。"
+        search-placeholder="搜索团队"
+        empty-title="还没有团队"
+        empty-description="新建团队,邀请 1–8 个 Agent 一起协作。"
         @select="selectSidebarItem"
         @more="openRoomManager"
         @context-menu="openRoomActions"
@@ -649,12 +649,12 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
     </template>
 
     <div v-if="groups.availability === 'unsupported' || groups.availability === 'unavailable'" class="groups-unavailable">
-      <EmptyState icon="alert" :title="groups.availability === 'unsupported' ? '群聊协议版本不兼容' : '群聊服务暂不可用'" :description="groups.error || `请确认 9119 已安装 YaoYao 群聊 protocol ${SUPPORTED_GROUP_PROTOCOL_VERSION_LABEL} 插件。`" action-label="重新检查" @action="groups.refresh" />
+      <EmptyState icon="alert" :title="groups.availability === 'unsupported' ? '团队协议版本不兼容' : '团队服务暂不可用'" :description="groups.error || `请确认 9119 已安装 YaoYao 团队协议 ${SUPPORTED_GROUP_PROTOCOL_VERSION_LABEL} 插件。`" action-label="重新检查" @action="groups.refresh" />
     </div>
     <div v-else class="groups-workspace">
       <MessageTimeline
         :messages="messages"
-        :title="groups.topicProtocol ? (groups.selectedTopic?.title || '新话题') : (groups.selectedRoom?.name || '群聊')"
+        :title="groups.topicProtocol ? (groups.selectedTopic?.title || '新话题') : (groups.selectedRoom?.name || '团队')"
         :subtitle="roomSubtitle"
         :loading="groups.isLoading"
         :has-older="groups.hasMoreBefore"
@@ -676,8 +676,8 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
         <template #header-actions>
           <div class="group-header-actions">
             <span v-if="hostAgent" class="group-host-chip" :title="`用户未明确 @ 时由 ${hostAgent.displayName || hostAgent.profile} 负责回应`">主持人 {{ hostAgent.displayName || hostAgent.profile }}</span>
-            <div class="group-avatars" aria-label="群聊成员"><AgentAvatar v-for="agent in agents.slice(0, 4)" :key="agent.id" :name="agent.name" :avatar="agent.nodeId === 'local' ? agentAvatars[agent.profile || ''] || '' : ''" :size="24" :title="agent.isHost ? `${agent.name} · 主持人` : agent.name" /><em v-if="agents.length > 4">+{{ agents.length - 4 }}</em></div>
-            <button class="icon-button" type="button" title="管理群聊" aria-label="管理群聊" :disabled="!groups.selectedRoom" @click="openSelectedRoomManager"><AppIcon name="dots" /></button>
+            <div class="group-avatars" aria-label="团队成员"><AgentAvatar v-for="agent in agents.slice(0, 4)" :key="agent.id" :name="agent.name" :avatar="agent.nodeId === 'local' ? agentAvatars[agent.profile || ''] || '' : ''" :size="24" :title="agent.isHost ? `${agent.name} · 主持人` : agent.name" /><em v-if="agents.length > 4">+{{ agents.length - 4 }}</em></div>
+            <button class="icon-button" type="button" title="管理团队" aria-label="管理团队" :disabled="!groups.selectedRoom" @click="openSelectedRoomManager"><AppIcon name="dots" /></button>
           </div>
         </template>
       </MessageTimeline>
@@ -694,7 +694,7 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
         :reference="reference"
         :mention-options="mentionOptions"
         :attachments-enabled="uploadsEnabled"
-        placeholder="发消息给群聊,输入 @ 提及 Agent"
+        placeholder="发消息给团队,输入 @ 提及 Agent"
         @send="send"
         @stop="stopActiveTopic"
         @tool-trace-toggle="toggleShowThinking"
@@ -731,14 +731,14 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
     </template>
   </WorkspaceView>
 
-  <FloatingResourceSearch section="groups" label="搜索群聊" :items="roomSidebarItems" @select="selectRoom" />
+  <FloatingResourceSearch section="groups" label="搜索团队" :items="roomSidebarItems" @select="selectRoom" />
   <CreateGroupDialog :open="createOpen" :profiles="profiles" :host-enabled="groups.hostProtocol" :host-flow-enabled="groups.hostFlowProtocol" :room-instructions-enabled="groups.roomInstructionsProtocol" :busy="groups.isLoading" @close="createOpen = false" @create="createRoom" />
   <PreviewModal v-if="preview" :item="preview" :items="conversationMediaItems" @close="preview = null" @add-to-composer="addPreviewToComposer" @source="preview = null" />
   <ImagePreviewLightbox v-model="mediaPreviewIndex" :images="lightboxMedia" :can-add="uploadsEnabled" @add="addMediaToComposer" />
 
   <Teleport to="body">
     <Transition name="group-menu">
-      <section v-if="roomActionMenu" class="group-room-actions" :style="roomActionMenuStyle" role="menu" aria-label="群聊房间操作" @contextmenu.prevent>
+      <section v-if="roomActionMenu" class="group-room-actions" :style="roomActionMenuStyle" role="menu" aria-label="团队操作" @contextmenu.prevent>
         <template v-if="roomActionMenu.topicId">
           <template v-if="topicActionRenaming">
             <label class="group-topic-rename">话题名称<input v-model="topicActionRenameValue" maxlength="120" autofocus @keydown.enter="renameTopicFromAction" /></label>
@@ -752,7 +752,7 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
         </template>
         <template v-else>
           <button class="group-action-row" role="menuitem" type="button" @click="startTopicFromRoomAction"><AppIcon name="plus" :size="14" />新建话题</button>
-          <button class="group-action-row danger" role="menuitem" type="button" @click="archiveRoomFromAction"><AppIcon name="archive" :size="14" />归档群聊</button>
+          <button class="group-action-row danger" role="menuitem" type="button" @click="archiveRoomFromAction"><AppIcon name="archive" :size="14" />归档团队</button>
         </template>
       </section>
     </Transition>
@@ -761,10 +761,10 @@ watch(() => auth.activeProfile?.name, profile => { if (profile) restoreShowThink
   <Teleport to="body">
     <Transition name="group-menu">
       <div v-if="archivedOverlayOpen" class="archived-overlay-backdrop" role="presentation" @click.self="archivedOverlayOpen = false">
-        <section class="archived-overlay" role="dialog" aria-modal="true" aria-label="已归档群聊和话题">
-          <header><div><small>群聊归档</small><strong>已归档内容</strong></div><button type="button" aria-label="关闭" @click="archivedOverlayOpen = false"><AppIcon name="close" :size="16" /></button></header>
-          <section class="archived-section"><h3>群聊</h3><p v-if="!archivedRoomList.length">没有已归档群聊</p><article v-for="archived in archivedRoomList" :key="archived.id"><span>{{ archived.name }}</span><button type="button" @click="restoreArchivedRoom(archived.id)">恢复</button></article></section>
-          <section class="archived-section"><h3>当前群聊的话题</h3><p v-if="!groups.selectedRoom">请先打开一个群聊查看其已归档话题</p><p v-else-if="!archivedTopicList.length">没有已归档话题</p><article v-for="topic in archivedTopicList" :key="topic.id"><span>{{ topic.title }}</span><button type="button" @click="restoreArchivedTopic(topic.id)">恢复</button></article></section>
+        <section class="archived-overlay" role="dialog" aria-modal="true" aria-label="已归档团队和话题">
+          <header><div><small>团队归档</small><strong>已归档内容</strong></div><button type="button" aria-label="关闭" @click="archivedOverlayOpen = false"><AppIcon name="close" :size="16" /></button></header>
+          <section class="archived-section"><h3>团队</h3><p v-if="!archivedRoomList.length">没有已归档团队</p><article v-for="archived in archivedRoomList" :key="archived.id"><span>{{ archived.name }}</span><button type="button" @click="restoreArchivedRoom(archived.id)">恢复</button></article></section>
+          <section class="archived-section"><h3>当前团队的话题</h3><p v-if="!groups.selectedRoom">请先打开一个团队查看其已归档话题</p><p v-else-if="!archivedTopicList.length">没有已归档话题</p><article v-for="topic in archivedTopicList" :key="topic.id"><span>{{ topic.title }}</span><button type="button" @click="restoreArchivedTopic(topic.id)">恢复</button></article></section>
         </section>
       </div>
     </Transition>

+ 1 - 1
tests/client/groupHost.test.ts

@@ -208,7 +208,7 @@ describe('group host controls', () => {
         roomInstructionsEnabled: true,
       },
     })
-    const instructions = wrapper.get<HTMLTextAreaElement>('textarea[aria-label="房间说明"]')
+    const instructions = wrapper.get<HTMLTextAreaElement>('textarea[aria-label="团队说明"]')
     await instructions.setValue('新规则\n输出中文')
     await instructions.trigger('change')
     expect(wrapper.emitted('updateRoom')?.at(-1)?.[0]).toMatchObject({

+ 28 - 28
tests/e2e/workspace.spec.ts

@@ -46,16 +46,16 @@ test('navigates every 9119 workspace without blank transitions', async ({ page }
   await expect(page.getByRole('option').first()).toContainText('夭夭 Web 验收会话')
   await expect(page.getByRole('option').first()).toContainText('夭夭')
   await expect(page.locator('.desktop-sidebar').getByText('已置顶 1', { exact: true })).toBeVisible()
-  await page.getByRole('button', { name: '群聊' }).click()
+  await page.getByRole('button', { name: '团队' }).click()
   await expect(page.getByRole('heading', { name: '设计验收' })).toBeVisible()
   await expect(page.getByRole('button', { name: '对话', exact: true })).toBeVisible()
-  await expect(page.getByRole('button', { name: '群聊', exact: true })).toHaveCount(0)
+  await expect(page.getByRole('button', { name: '团队', exact: true })).toHaveCount(0)
   await expect(page.getByRole('button', { name: '文件库', exact: true })).toHaveCount(0)
   await expect(page.getByRole('button', { name: '产物', exact: true })).toHaveCount(0)
-  await expect(page.getByRole('button', { name: '新建群聊' })).toBeVisible()
-  await expect(page.getByRole('button', { name: '新建群聊' })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)')
-  await expect(page.getByRole('button', { name: '新建群聊' })).toHaveCSS('font-weight', '580')
-  await expect(page.getByRole('button', { name: '管理群聊' })).toBeVisible()
+  await expect(page.getByRole('button', { name: '新建团队' })).toBeVisible()
+  await expect(page.getByRole('button', { name: '新建团队' })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)')
+  await expect(page.getByRole('button', { name: '新建团队' })).toHaveCSS('font-weight', '580')
+  await expect(page.getByRole('button', { name: '管理团队' })).toBeVisible()
   await expect(page.getByRole('button', { name: '发送消息' })).toBeVisible()
 
   await page.goto('/files')
@@ -105,13 +105,13 @@ test('uses the active session or group title in the browser title', async ({ pag
 })
 
 test('uses a floating search dialog for group rooms', async ({ page }) => {
-  await page.getByRole('button', { name: '群聊' }).click()
+  await page.getByRole('button', { name: '团队' }).click()
   await expect(page.getByRole('heading', { name: '设计验收' })).toBeVisible()
 
   await page.locator('.desktop-sidebar').getByRole('button', { name: '搜索', exact: true }).click()
-  const searchDialog = page.getByRole('dialog', { name: '搜索群聊' })
+  const searchDialog = page.getByRole('dialog', { name: '搜索团队' })
   await expect(searchDialog).toBeVisible()
-  await searchDialog.getByPlaceholder('搜索群聊').fill('设计')
+  await searchDialog.getByPlaceholder('搜索团队').fill('设计')
   const roomResult = searchDialog.getByRole('option', { name: /设计与工程协作/ })
   await expect(roomResult).toBeVisible()
   await roomResult.click()
@@ -119,17 +119,17 @@ test('uses a floating search dialog for group rooms', async ({ page }) => {
 })
 
 test('selects one protocol v5 host independently from no-mention replies', async ({ page }) => {
-  await page.getByRole('button', { name: '群聊' }).click()
+  await page.getByRole('button', { name: '团队' }).click()
   await expect(page.locator('.group-host-chip')).toHaveText('主持人 夭夭')
 
-  await page.getByRole('button', { name: '新建群聊' }).click()
-  const createDialog = page.getByRole('dialog', { name: '新建群聊' })
+  await page.getByRole('button', { name: '新建团队' }).click()
+  const createDialog = page.getByRole('dialog', { name: '新建团队' })
   await createDialog.getByRole('button', { name: /yaoer/ }).click()
   await createDialog.getByLabel('主持人').selectOption('yaoer')
   await expect(createDialog.getByLabel('所有成员无需 @ 也回复')).toBeChecked()
   await createDialog.getByRole('button', { name: '关闭' }).click()
 
-  await page.getByRole('button', { name: '管理群聊' }).click()
+  await page.getByRole('button', { name: '管理团队' }).click()
   const manager = page.locator('.group-manager')
   await expect(manager.getByLabel('主持人')).toHaveValue('33333333-3333-4333-8333-333333333333')
   const promoteRequest = page.waitForRequest(request => request.method() === 'PATCH' && request.url().endsWith('/agents/34343434-3434-4434-8434-343434343434'))
@@ -145,11 +145,11 @@ test('selects one protocol v5 host independently from no-mention replies', async
 })
 
 test('edits every protocol v5 Agent setting with one inspector close control', async ({ page }) => {
-  await page.getByRole('button', { name: '群聊' }).click()
+  await page.getByRole('button', { name: '团队' }).click()
   await expect(page.getByRole('heading', { name: '设计验收' })).toBeVisible()
-  await page.getByRole('button', { name: '管理群聊' }).click()
+  await page.getByRole('button', { name: '管理团队' }).click()
 
-  await expect(page.getByRole('button', { name: '关闭群聊管理' })).toHaveCount(1)
+  await expect(page.getByRole('button', { name: '关闭团队管理' })).toHaveCount(1)
   await expect(page.getByRole('button', { name: '关闭预览' })).toHaveCount(0)
   await expect(page.getByLabel('最多回复轮数')).toHaveValue('-1')
   await page.getByRole('button', { name: '设置夭夭' }).click()
@@ -193,9 +193,9 @@ test('edits every protocol v5 Agent setting with one inspector close control', a
 })
 
 test('pins the named Agent typing status above the group composer', async ({ page }) => {
-  await page.getByRole('button', { name: '群聊' }).click()
+  await page.getByRole('button', { name: '团队' }).click()
   const composer = page.locator('.composer-area')
-  await page.getByRole('textbox', { name: '发消息给群聊,输入 @ 提及 Agent' }).fill('@夭夭 检查输入状态')
+  await page.getByRole('textbox', { name: '发消息给团队,输入 @ 提及 Agent' }).fill('@夭夭 检查输入状态')
   await page.getByRole('button', { name: '发送消息' }).click()
 
   const typing = composer.getByRole('status', { name: 'Agent 输入状态' })
@@ -210,7 +210,7 @@ test('pins the named Agent typing status above the group composer', async ({ pag
 })
 
 test('reconnects the group event stream after an unexpected close', async ({ page }) => {
-  await page.getByRole('button', { name: '群聊' }).click()
+  await page.getByRole('button', { name: '团队' }).click()
   await expect(page.getByText('已同步', { exact: true })).toBeVisible()
   const before = await (await page.request.get('http://127.0.0.1:19119/__test/group-connections')).json() as { count: number }
   await page.request.post('http://127.0.0.1:19119/__test/groups/disconnect')
@@ -223,8 +223,8 @@ test('reconnects the group event stream after an unexpected close', async ({ pag
 
 test('recovers the group page after its initial upstream connection fails', async ({ page }) => {
   await page.request.post('http://127.0.0.1:19119/__test/groups/availability', { data: { available: false } })
-  await page.getByRole('button', { name: '群聊' }).click()
-  await expect(page.getByRole('heading', { name: '群聊服务暂不可用' })).toBeVisible()
+  await page.getByRole('button', { name: '团队' }).click()
+  await expect(page.getByRole('heading', { name: '团队服务暂不可用' })).toBeVisible()
   await page.request.post('http://127.0.0.1:19119/__test/groups/availability', { data: { available: true } })
   await expect(page.getByRole('heading', { name: '设计验收' })).toBeVisible({ timeout: 5_000 })
 })
@@ -531,7 +531,7 @@ test('renders historical assistant MEDIA as Markdown in chat and group chat', as
   await expect(reopenedMediaDialog).toBeHidden()
   await expect(page.locator('.composer-attachments img[alt="AppIcon-1024.png"]')).toBeVisible()
 
-  await page.getByRole('button', { name: '群聊' }).click()
+  await page.getByRole('button', { name: '团队' }).click()
   await expect(page.locator('.markdown img[alt="AppIcon-1024.png"]')).toBeVisible()
 })
 
@@ -540,19 +540,19 @@ test('selects existing group topics and creates a new protocol v4 topic on first
   const existingTopicId = '88888888-8888-4888-8888-888888888888'
   const existingTopicSidebarId = `topic:${roomId}:${existingTopicId}`
 
-  await page.getByRole('button', { name: '群聊' }).click()
+  await page.getByRole('button', { name: '团队' }).click()
   const sidebar = page.locator('.desktop-sidebar')
   const roomItem = sidebar.locator(`[data-sidebar-id="${roomId}"]`)
   const existingTopic = sidebar.locator(`[data-sidebar-id="${existingTopicSidebarId}"]`)
   await roomItem.click()
   const backToRooms = sidebar.locator('[data-sidebar-id="group-list"]')
-  await expect(backToRooms).toContainText('返回房间列表')
+  await expect(backToRooms).toContainText('返回团队列表')
   await expect(sidebar.getByRole('button', { name: '返回对话' })).toBeVisible()
   await expect(sidebar.getByRole('button', { name: '折叠侧边栏' })).toBeVisible()
   await expect(sidebar.getByText('设计与工程协作', { exact: true })).toBeVisible()
   await expect(sidebar.getByText('2 个 Agent', { exact: true })).toBeVisible()
   await expect(sidebar.getByRole('button', { name: '搜索', exact: true })).toBeHidden()
-  await expect(sidebar.getByRole('button', { name: '新建群聊' })).toBeHidden()
+  await expect(sidebar.getByRole('button', { name: '新建团队' })).toBeHidden()
   await expect(sidebar.getByRole('button', { name: '已归档' })).toBeHidden()
   await expect(sidebar.getByRole('button', { name: '对话', exact: true })).toBeHidden()
   await expect(sidebar.locator('.sidebar-footer')).toBeHidden()
@@ -580,9 +580,9 @@ test('selects existing group topics and creates a new protocol v4 topic on first
 
   await backToRooms.click()
   await expect(sidebar.getByRole('button', { name: '搜索', exact: true })).toBeVisible()
-  await expect(sidebar.getByRole('button', { name: '新建群聊' })).toBeVisible()
+  await expect(sidebar.getByRole('button', { name: '新建团队' })).toBeVisible()
   await roomItem.click({ button: 'right' })
-  const roomActions = page.getByRole('menu', { name: '群聊房间操作' })
+  const roomActions = page.getByRole('menu', { name: '团队操作' })
   await roomActions.getByRole('menuitem', { name: '新建话题' }).click()
   const draftTopic = sidebar.locator('.sidebar-item--topic').filter({ hasText: '新话题' })
   await expect(draftTopic).toBeVisible()
@@ -597,7 +597,7 @@ test('selects existing group topics and creates a new protocol v4 topic on first
     const url = new URL(request.url())
     return request.method() === 'POST' && url.pathname === `/api/app/groups/rooms/${roomId}/messages`
   })
-  await page.getByRole('textbox', { name: '发消息给群聊,输入 @ 提及 Agent' }).fill(content)
+  await page.getByRole('textbox', { name: '发消息给团队,输入 @ 提及 Agent' }).fill(content)
   await page.getByRole('button', { name: '发送消息' }).click()
   const sendRequest = await sendRequestPromise
   const sentPayload = sendRequest.postDataJSON() as { topicId: string; content: string; clientMessageId: string }