mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(agents): manage AGENTS.md from the agent editor + clarify context-file roles
Add a friendly AGENTS.md section editor inside the edit-agent modal's "高级" (Advanced) collapsible, alongside the existing additional-instructions field. New AgentGuideEditor reuses MemorySection cards, parses the file into ## sections, and saves whole-file via the workspace API (no backend change). Empty files offer a "create" scaffold flow. Reword the collapsible to a generic "高级" and add a role-clarifying note that fixes the boundary: factory identity goes in the form fields, evolving memory/rules go in the context files. Also fix the additional-instructions textarea so it fills the form width. Closes matevip/mateclaw#290
This commit is contained in:
parent
c7ae406dcc
commit
4f11a444c1
@ -1221,7 +1221,8 @@ export default {
|
||||
taglinePreview: 'Card preview:',
|
||||
backstory: 'Backstory',
|
||||
backstoryHint: 'Invisible to users, felt in every conversation. Their experience, beliefs, and working principles.',
|
||||
extraInstructions: 'Advanced: Additional Instructions',
|
||||
advanced: 'Advanced',
|
||||
extraInstructions: 'Additional Instructions',
|
||||
extraInstructionsHint: 'Optional. Use for output format, process checklists, or boundary rules.',
|
||||
maxIterations: 'Max Iterations',
|
||||
defaultThinkingLevel: 'Default Thinking Level',
|
||||
@ -1235,6 +1236,16 @@ export default {
|
||||
tagUndo: 'Undo',
|
||||
enabled: 'Enabled',
|
||||
},
|
||||
guide: {
|
||||
summary: 'Behavior Guide (AGENTS.md)',
|
||||
desc: "This is the employee's behavior handbook (AGENTS.md): when to write memories, what goes in each file, and which memory tools are available. The Role/Goal/Backstory above are the employee's factory identity; evolving memories like PROFILE/MEMORY live in the Memory menu.",
|
||||
empty: 'This employee has no behavior guide (AGENTS.md) yet.',
|
||||
create: 'Create Behavior Guide',
|
||||
saved: 'Saved',
|
||||
addSection: 'Add Section',
|
||||
addSectionPlaceholder: 'Section title',
|
||||
scaffold: '## When to Write Memories\n\n(Describe when this employee should record to MEMORY.md / PROFILE.md.)\n\n## File Responsibilities\n\n(Describe what belongs in SOUL.md / PROFILE.md / MEMORY.md.)\n',
|
||||
},
|
||||
thinkingLevels: {
|
||||
auto: 'Follow Model',
|
||||
off: 'Off',
|
||||
|
||||
@ -1113,7 +1113,8 @@ export default {
|
||||
taglinePreview: '卡片预览:',
|
||||
backstory: '背景(Backstory)',
|
||||
backstoryHint: '用户看不见,但每一次对话都能感觉到。写他的经验、信念、做事原则。',
|
||||
extraInstructions: '高级:补充指令',
|
||||
advanced: '高级',
|
||||
extraInstructions: '补充指令',
|
||||
extraInstructionsHint: '可选。用于细化输出格式、流程清单或边界规则。',
|
||||
maxIterations: '最大迭代次数',
|
||||
defaultThinkingLevel: '默认思考深度',
|
||||
@ -1127,6 +1128,16 @@ export default {
|
||||
tagUndo: '撤销',
|
||||
enabled: '启用',
|
||||
},
|
||||
guide: {
|
||||
summary: '行为指南(AGENTS.md)',
|
||||
desc: '这里是该员工的行为手册(AGENTS.md):规定何时写记忆、各文件放什么、可用的记忆工具。上方的岗位/目标/背景是员工的出厂身份;PROFILE/MEMORY 等可演化的记忆在「记忆」菜单管理。',
|
||||
empty: '该员工还没有行为指南(AGENTS.md)。',
|
||||
create: '创建行为指南',
|
||||
saved: '已保存',
|
||||
addSection: '新增小节',
|
||||
addSectionPlaceholder: '小节标题',
|
||||
scaffold: '## 何时写记忆\n\n(说明这个员工在什么情况下应当记录到 MEMORY.md / PROFILE.md。)\n\n## 文件职责\n\n(说明 SOUL.md / PROFILE.md / MEMORY.md 各自该放什么内容。)\n',
|
||||
},
|
||||
thinkingLevels: {
|
||||
auto: '跟随模型',
|
||||
off: '关闭',
|
||||
|
||||
@ -341,10 +341,16 @@
|
||||
<div class="form-group full-width">
|
||||
<details class="advanced-prompt">
|
||||
<summary class="advanced-prompt__summary">
|
||||
{{ t('agents.fields.extraInstructions') }}
|
||||
{{ t('agents.fields.advanced') }}
|
||||
</summary>
|
||||
<label class="form-label">{{ t('agents.fields.extraInstructions') }}</label>
|
||||
<textarea v-model="profileForm.extra" class="form-textarea" rows="4" :placeholder="t('agents.placeholders.extraInstructions')"></textarea>
|
||||
<p class="form-hint">{{ t('agents.fields.extraInstructionsHint') }}</p>
|
||||
<div v-if="editingAgent" class="advanced-guide">
|
||||
<label class="form-label">{{ t('agents.guide.summary') }}</label>
|
||||
<p class="form-hint">{{ t('agents.guide.desc') }}</p>
|
||||
<AgentGuideEditor :agent-id="editingAgent.id" />
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -669,6 +675,7 @@ import type { Agent } from '@/types/index'
|
||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||
import SkillIconPicker from '@/components/common/SkillIconPicker.vue'
|
||||
import LivePanel from '@/components/live/LivePanel.vue'
|
||||
import AgentGuideEditor from './Agents/components/AgentGuideEditor.vue'
|
||||
import {
|
||||
emptyProfile,
|
||||
parsePrompt,
|
||||
@ -1957,6 +1964,8 @@ html.dark .seg-count.warn {
|
||||
}
|
||||
.advanced-prompt[open] > .advanced-prompt__summary { padding: 0 0 8px; border-bottom: 1px solid var(--mc-border-light); margin-bottom: 8px; }
|
||||
.advanced-prompt[open] > .advanced-prompt__summary::before { transform: rotate(90deg); }
|
||||
.advanced-prompt .form-textarea { width: 100%; box-sizing: border-box; }
|
||||
.advanced-guide { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--mc-border-light); }
|
||||
.modal-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 16px 24px; border-top: 1px solid var(--mc-border-light); }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
157
mateclaw-ui/src/views/Agents/components/AgentGuideEditor.vue
Normal file
157
mateclaw-ui/src/views/Agents/components/AgentGuideEditor.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="guide-editor">
|
||||
<div v-if="loading" class="guide-state">…</div>
|
||||
|
||||
<!-- Empty state: AGENTS.md not present or blank -->
|
||||
<div v-else-if="sections.length === 0" class="guide-empty">
|
||||
<p class="guide-empty__text">{{ t('agents.guide.empty') }}</p>
|
||||
<button class="guide-create-btn" :disabled="saving" @click="createGuide">
|
||||
{{ t('agents.guide.create') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Section cards -->
|
||||
<div v-else class="guide-sections">
|
||||
<MemorySection
|
||||
v-for="(sec, idx) in sections"
|
||||
:key="idx"
|
||||
:section="sec"
|
||||
:save-handler="(body: string) => saveSection(idx, body)"
|
||||
/>
|
||||
|
||||
<div class="guide-add">
|
||||
<input
|
||||
v-model="newHeading"
|
||||
class="guide-add__input"
|
||||
:placeholder="t('agents.guide.addSectionPlaceholder')"
|
||||
@keyup.enter="addSection"
|
||||
/>
|
||||
<button class="guide-add__btn" :disabled="!newHeading.trim() || saving" @click="addSection">
|
||||
+ {{ t('agents.guide.addSection') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { agentContextApi } from '@/api/index'
|
||||
import { mcToast } from '@/composables/useMcToast'
|
||||
import MemorySection from '@/views/Memory/components/MemorySection.vue'
|
||||
import type { MemorySectionData } from '@/views/Memory/components/types'
|
||||
|
||||
const props = defineProps<{ agentId: string | number }>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const FILE = 'AGENTS.md'
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const sections = ref<MemorySectionData[]>([])
|
||||
const newHeading = ref('')
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await agentContextApi.getFile(props.agentId, FILE)
|
||||
sections.value = parseSections(res?.data?.content || '')
|
||||
} catch {
|
||||
// File may not exist yet — treat as empty so the create flow shows.
|
||||
sections.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Split into `## ` sections; content before the first heading is a synthetic preamble. */
|
||||
function parseSections(content: string): MemorySectionData[] {
|
||||
if (!content.trim()) return []
|
||||
const result: MemorySectionData[] = []
|
||||
for (const part of content.split(/(?=^## )/m)) {
|
||||
const match = part.match(/^## (.+)\n([\s\S]*)/)
|
||||
if (match) {
|
||||
result.push({ heading: match[1].trim(), body: match[2].trim(), userEdited: false })
|
||||
} else if (part.trim() && result.length === 0) {
|
||||
result.push({ heading: t('memory.memoryBrowser.header'), body: part.trim(), userEdited: false, synthetic: true })
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Re-serialize the local sections back into a whole AGENTS.md document. */
|
||||
function serialize(secs: MemorySectionData[]): string {
|
||||
return secs
|
||||
.map(s => (s.synthetic ? s.body.trim() : `## ${s.heading}\n${s.body.trim()}`))
|
||||
.filter(Boolean)
|
||||
.join('\n\n') + '\n'
|
||||
}
|
||||
|
||||
async function saveSection(idx: number, body: string) {
|
||||
const next = sections.value.map((s, i) => (i === idx ? { ...s, body } : s))
|
||||
await agentContextApi.saveFile(props.agentId, FILE, serialize(next))
|
||||
mcToast.success(t('agents.guide.saved'))
|
||||
await load()
|
||||
}
|
||||
|
||||
async function createGuide() {
|
||||
saving.value = true
|
||||
try {
|
||||
await agentContextApi.saveFile(props.agentId, FILE, t('agents.guide.scaffold'))
|
||||
mcToast.success(t('agents.guide.saved'))
|
||||
await load()
|
||||
} catch (e: any) {
|
||||
mcToast.error(e?.message || 'Save failed')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addSection() {
|
||||
const heading = newHeading.value.trim()
|
||||
if (!heading) return
|
||||
saving.value = true
|
||||
try {
|
||||
const next = [...sections.value, { heading, body: '', userEdited: false }]
|
||||
await agentContextApi.saveFile(props.agentId, FILE, serialize(next))
|
||||
newHeading.value = ''
|
||||
await load()
|
||||
} catch (e: any) {
|
||||
mcToast.error(e?.message || 'Save failed')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.guide-editor { margin-top: 8px; }
|
||||
.guide-state { font-size: 13px; color: var(--mc-text-tertiary); padding: 8px 0; }
|
||||
.guide-empty {
|
||||
display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
|
||||
padding: 14px 16px; border-radius: 12px; background: var(--mc-bg-sunken);
|
||||
}
|
||||
.guide-empty__text { margin: 0; font-size: 13px; color: var(--mc-text-secondary); }
|
||||
.guide-create-btn {
|
||||
padding: 6px 14px; border-radius: 8px; font-size: 12px; font-weight: 500;
|
||||
background: var(--mc-primary); color: #fff; border: 1px solid var(--mc-primary);
|
||||
cursor: pointer; transition: all 0.12s;
|
||||
}
|
||||
.guide-create-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.guide-sections { display: flex; flex-direction: column; gap: 10px; }
|
||||
.guide-add { display: flex; gap: 8px; align-items: center; }
|
||||
.guide-add__input {
|
||||
flex: 1; padding: 8px 12px; border: 1px solid var(--mc-border); border-radius: 8px;
|
||||
background: var(--mc-bg-elevated); font-size: 13px; color: var(--mc-text-primary); outline: none;
|
||||
}
|
||||
.guide-add__input:focus { border-color: var(--mc-primary); }
|
||||
.guide-add__btn {
|
||||
padding: 8px 14px; border-radius: 8px; font-size: 12px; font-weight: 500;
|
||||
background: transparent; color: var(--mc-text-secondary); border: 1px solid var(--mc-border);
|
||||
cursor: pointer; white-space: nowrap; transition: all 0.12s;
|
||||
}
|
||||
.guide-add__btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.guide-add__btn:not(:disabled):hover { border-color: var(--mc-primary); color: var(--mc-primary); }
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user