ConfigView.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <script setup lang="ts">
  2. import { ref, onMounted } from 'vue'
  3. import { useRouter } from 'vue-router'
  4. import { Card, List, Input, Button, message, Empty, Tag, Modal, Checkbox } from 'ant-design-vue'
  5. import { configApi, type ConfigFile } from '@/api/config'
  6. import { SaveOutlined, FileTextOutlined, ReloadOutlined } from '@ant-design/icons-vue'
  7. const router = useRouter()
  8. const configs = ref<string[]>([])
  9. const selectedConfig = ref<ConfigFile | null>(null)
  10. const editingContent = ref('')
  11. const loading = ref(false)
  12. const saving = ref(false)
  13. const resetting = ref(false)
  14. const showResetModal = ref(false)
  15. const resetOptions = ref({
  16. reset_sessions: true,
  17. reset_memory: true,
  18. reset_global_config: false,
  19. })
  20. const configDescriptions: Record<string, string> = {
  21. CONFIG: '全局配置',
  22. IDENTITY: '身份定义',
  23. USER: '用户信息',
  24. SOUL: '人格模板',
  25. MEMORY: '长期记忆',
  26. AGENTS: '工作空间规则',
  27. HEARTBEAT: '心跳任务',
  28. BOOTSTRAP: '初始化引导',
  29. }
  30. // 获取配置文件的后缀
  31. const getConfigExtension = (name: string): string => {
  32. return name === 'CONFIG' ? '.json' : '.md'
  33. }
  34. const loadConfigs = async () => {
  35. loading.value = true
  36. try {
  37. const res = await configApi.list()
  38. configs.value = res.configs
  39. } catch (error) {
  40. message.error('加载配置列表失败')
  41. } finally {
  42. loading.value = false
  43. }
  44. }
  45. const selectConfig = async (name: string) => {
  46. try {
  47. const res = await configApi.get(name)
  48. selectedConfig.value = res
  49. editingContent.value = res.content
  50. } catch (error) {
  51. message.error('加载配置失败')
  52. }
  53. }
  54. const saveConfig = async () => {
  55. if (!selectedConfig.value) return
  56. saving.value = true
  57. try {
  58. await configApi.update(selectedConfig.value.name, editingContent.value)
  59. message.success('保存成功')
  60. } catch (error: any) {
  61. // 透传后端错误信息
  62. const errorMsg = error?.response?.data?.detail || error?.message || '保存失败'
  63. message.error(errorMsg)
  64. } finally {
  65. saving.value = false
  66. }
  67. }
  68. const confirmReset = () => {
  69. // 重置选项为默认值
  70. resetOptions.value = {
  71. reset_sessions: true,
  72. reset_memory: true,
  73. reset_global_config: false,
  74. }
  75. showResetModal.value = true
  76. }
  77. const handleReset = async () => {
  78. resetting.value = true
  79. try {
  80. const res = await configApi.reset(resetOptions.value)
  81. message.success(res.message)
  82. showResetModal.value = false
  83. selectedConfig.value = null
  84. editingContent.value = ''
  85. // 如果清除了会话历史,也要清除 localStorage 中的上次会话 ID
  86. if (resetOptions.value.reset_sessions) {
  87. localStorage.removeItem('helloclaw.lastSessionId')
  88. }
  89. await loadConfigs()
  90. // 导航到聊天页面并传递刷新参数,让 ChatView 重新获取 agent 信息
  91. router.push({ name: 'chat', query: { refresh: Date.now().toString() } })
  92. } catch (error) {
  93. message.error('重置失败')
  94. } finally {
  95. resetting.value = false
  96. }
  97. }
  98. onMounted(() => {
  99. loadConfigs()
  100. })
  101. </script>
  102. <template>
  103. <div class="config-view">
  104. <div class="config-header">
  105. <h1>配置管理</h1>
  106. <p>管理 Agent 的配置文件和身份信息</p>
  107. </div>
  108. <div class="config-content">
  109. <!-- 配置列表 -->
  110. <div class="config-list">
  111. <Card :loading="loading" class="list-card">
  112. <template #title>
  113. <FileTextOutlined /> 配置文件
  114. </template>
  115. <template #extra>
  116. <button
  117. class="reset-btn"
  118. @click="confirmReset"
  119. title="重置为初始模板"
  120. >
  121. <ReloadOutlined /> 初始化
  122. </button>
  123. </template>
  124. <List :data-source="configs" :locale="{ emptyText: '暂无配置文件' }">
  125. <template #renderItem="{ item }">
  126. <List.Item
  127. @click="selectConfig(item)"
  128. :class="['config-item', { active: selectedConfig?.name === item }]"
  129. >
  130. <div class="config-item-content">
  131. <span class="config-name">{{ item }}</span>
  132. <Tag color="error" v-if="configDescriptions[item]">
  133. {{ configDescriptions[item] }}
  134. </Tag>
  135. </div>
  136. </List.Item>
  137. </template>
  138. </List>
  139. </Card>
  140. </div>
  141. <!-- 编辑区域 -->
  142. <div class="config-editor">
  143. <Card v-if="selectedConfig" class="editor-card">
  144. <template #title>
  145. <span>{{ selectedConfig.name }}</span>
  146. <Tag color="green" style="margin-left: 8px">{{ getConfigExtension(selectedConfig.name) }}</Tag>
  147. </template>
  148. <template #extra>
  149. <Button
  150. type="primary"
  151. :loading="saving"
  152. @click="saveConfig"
  153. >
  154. <SaveOutlined /> 保存
  155. </Button>
  156. </template>
  157. <Input.TextArea
  158. v-model:value="editingContent"
  159. :auto-size="{ minRows: 18, maxRows: 30 }"
  160. class="editor-textarea"
  161. />
  162. </Card>
  163. <Card v-else class="empty-card">
  164. <Empty
  165. description="请从左侧选择一个配置文件"
  166. :image-style="{ height: '80px' }"
  167. />
  168. </Card>
  169. </div>
  170. </div>
  171. <!-- 重置确认弹窗 -->
  172. <Modal
  173. v-model:open="showResetModal"
  174. title="确认初始化"
  175. :confirm-loading="resetting"
  176. @ok="handleReset"
  177. okText="确认初始化"
  178. cancelText="取消"
  179. okType="danger"
  180. >
  181. <div class="reset-warning">
  182. <p style="color: #ff4d4f; font-weight: 500;">⚠️ 警告:此操作不可撤销!</p>
  183. <p>初始化将把所有配置文件恢复为默认模板,包括:</p>
  184. <ul>
  185. <li>AGENTS.md - 工作空间规则</li>
  186. <li>IDENTITY.md - 身份信息</li>
  187. <li>USER.md - 用户信息</li>
  188. <li>SOUL.md - 人格模板</li>
  189. <li>MEMORY.md - 长期记忆</li>
  190. <li>HEARTBEAT.md - 心跳任务</li>
  191. <li>BOOTSTRAP.md - 初始化引导</li>
  192. </ul>
  193. <div class="reset-options">
  194. <p style="font-weight: 500; margin-bottom: 8px;">额外清除选项:</p>
  195. <Checkbox v-model:checked="resetOptions.reset_sessions">
  196. 清除所有会话历史
  197. </Checkbox>
  198. <Checkbox v-model:checked="resetOptions.reset_memory">
  199. 清除每日记忆文件
  200. </Checkbox>
  201. <Checkbox v-model:checked="resetOptions.reset_global_config">
  202. 重置全局配置(LLM、Agent 设置等)
  203. </Checkbox>
  204. </div>
  205. <p style="margin-top: 16px;">您确定要继续吗?</p>
  206. </div>
  207. </Modal>
  208. </div>
  209. </template>
  210. <style scoped>
  211. .config-view {
  212. min-height: 100%;
  213. width: 100%;
  214. display: flex;
  215. flex-direction: column;
  216. padding: 24px;
  217. box-sizing: border-box;
  218. }
  219. .config-header {
  220. flex-shrink: 0;
  221. margin-bottom: 24px;
  222. }
  223. .config-header h1 {
  224. margin: 0 0 8px;
  225. font-size: 24px;
  226. font-weight: 500;
  227. }
  228. .config-header p {
  229. margin: 0;
  230. color: #999;
  231. }
  232. .config-content {
  233. display: flex;
  234. gap: 24px;
  235. flex: 1;
  236. min-height: 0;
  237. overflow: hidden;
  238. }
  239. .config-list {
  240. width: 280px;
  241. flex-shrink: 0;
  242. display: flex;
  243. flex-direction: column;
  244. }
  245. .list-card {
  246. flex: 1;
  247. display: flex;
  248. flex-direction: column;
  249. overflow: hidden;
  250. }
  251. .list-card :deep(.ant-card-body) {
  252. flex: 1;
  253. padding: 0;
  254. overflow-y: auto;
  255. }
  256. .config-item {
  257. cursor: pointer;
  258. padding: 12px 16px;
  259. transition: all 0.2s;
  260. border-bottom: 1px solid #f0f0f0;
  261. }
  262. .config-item:hover {
  263. background-color: #f5f5f5;
  264. }
  265. .config-item.active {
  266. background-color: #fff1f0;
  267. border-left: 3px solid #ff4d4f;
  268. }
  269. .config-item-content {
  270. display: flex;
  271. flex-direction: column;
  272. gap: 4px;
  273. }
  274. .config-name {
  275. font-weight: 500;
  276. }
  277. .config-editor {
  278. flex: 1;
  279. min-width: 0;
  280. display: flex;
  281. flex-direction: column;
  282. }
  283. .editor-card {
  284. flex: 1;
  285. display: flex;
  286. flex-direction: column;
  287. overflow: hidden;
  288. }
  289. .editor-card :deep(.ant-card-head) {
  290. flex-shrink: 0;
  291. }
  292. .editor-card :deep(.ant-card-body) {
  293. flex: 1;
  294. overflow: hidden;
  295. display: flex;
  296. flex-direction: column;
  297. }
  298. .editor-textarea {
  299. flex: 1;
  300. width: 100%;
  301. font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
  302. font-size: 13px;
  303. line-height: 1.6;
  304. resize: none;
  305. }
  306. .empty-card {
  307. flex: 1;
  308. display: flex;
  309. align-items: center;
  310. justify-content: center;
  311. }
  312. /* 初始化按钮 - 纯红色背景 + 白色字体(可操作) */
  313. .reset-btn {
  314. padding: 4px 12px;
  315. font-size: 13px;
  316. border: none;
  317. border-radius: 6px;
  318. background: #ff4d4f;
  319. color: #fff;
  320. cursor: pointer;
  321. transition: all 0.2s ease;
  322. display: inline-flex;
  323. align-items: center;
  324. gap: 4px;
  325. }
  326. .reset-btn:hover {
  327. background: #ff7875;
  328. }
  329. .reset-warning {
  330. padding: 8px 0;
  331. }
  332. .reset-warning ul {
  333. margin: 12px 0;
  334. padding-left: 24px;
  335. }
  336. .reset-warning li {
  337. margin: 4px 0;
  338. color: #666;
  339. }
  340. .reset-options {
  341. margin-top: 16px;
  342. padding: 12px;
  343. background: #fafafa;
  344. border-radius: 6px;
  345. display: flex;
  346. flex-direction: column;
  347. gap: 8px;
  348. }
  349. </style>