mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +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:',
|
taglinePreview: 'Card preview:',
|
||||||
backstory: 'Backstory',
|
backstory: 'Backstory',
|
||||||
backstoryHint: 'Invisible to users, felt in every conversation. Their experience, beliefs, and working principles.',
|
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.',
|
extraInstructionsHint: 'Optional. Use for output format, process checklists, or boundary rules.',
|
||||||
maxIterations: 'Max Iterations',
|
maxIterations: 'Max Iterations',
|
||||||
defaultThinkingLevel: 'Default Thinking Level',
|
defaultThinkingLevel: 'Default Thinking Level',
|
||||||
@ -1235,6 +1236,16 @@ export default {
|
|||||||
tagUndo: 'Undo',
|
tagUndo: 'Undo',
|
||||||
enabled: 'Enabled',
|
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: {
|
thinkingLevels: {
|
||||||
auto: 'Follow Model',
|
auto: 'Follow Model',
|
||||||
off: 'Off',
|
off: 'Off',
|
||||||
|
|||||||
@ -1113,7 +1113,8 @@ export default {
|
|||||||
taglinePreview: '卡片预览:',
|
taglinePreview: '卡片预览:',
|
||||||
backstory: '背景(Backstory)',
|
backstory: '背景(Backstory)',
|
||||||
backstoryHint: '用户看不见,但每一次对话都能感觉到。写他的经验、信念、做事原则。',
|
backstoryHint: '用户看不见,但每一次对话都能感觉到。写他的经验、信念、做事原则。',
|
||||||
extraInstructions: '高级:补充指令',
|
advanced: '高级',
|
||||||
|
extraInstructions: '补充指令',
|
||||||
extraInstructionsHint: '可选。用于细化输出格式、流程清单或边界规则。',
|
extraInstructionsHint: '可选。用于细化输出格式、流程清单或边界规则。',
|
||||||
maxIterations: '最大迭代次数',
|
maxIterations: '最大迭代次数',
|
||||||
defaultThinkingLevel: '默认思考深度',
|
defaultThinkingLevel: '默认思考深度',
|
||||||
@ -1127,6 +1128,16 @@ export default {
|
|||||||
tagUndo: '撤销',
|
tagUndo: '撤销',
|
||||||
enabled: '启用',
|
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: {
|
thinkingLevels: {
|
||||||
auto: '跟随模型',
|
auto: '跟随模型',
|
||||||
off: '关闭',
|
off: '关闭',
|
||||||
|
|||||||
@ -341,10 +341,16 @@
|
|||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<details class="advanced-prompt">
|
<details class="advanced-prompt">
|
||||||
<summary class="advanced-prompt__summary">
|
<summary class="advanced-prompt__summary">
|
||||||
{{ t('agents.fields.extraInstructions') }}
|
{{ t('agents.fields.advanced') }}
|
||||||
</summary>
|
</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>
|
<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>
|
<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>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -669,6 +675,7 @@ import type { Agent } from '@/types/index'
|
|||||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||||
import SkillIconPicker from '@/components/common/SkillIconPicker.vue'
|
import SkillIconPicker from '@/components/common/SkillIconPicker.vue'
|
||||||
import LivePanel from '@/components/live/LivePanel.vue'
|
import LivePanel from '@/components/live/LivePanel.vue'
|
||||||
|
import AgentGuideEditor from './Agents/components/AgentGuideEditor.vue'
|
||||||
import {
|
import {
|
||||||
emptyProfile,
|
emptyProfile,
|
||||||
parsePrompt,
|
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 { 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[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); }
|
.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) {
|
@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