Add lightweight goal mode reflect workflow
This commit is contained in:
1 parent
639da67916
commit
1a7d54b82f
6 files changed
+317
-2
No files matched your search
@@ -59,6 +59,9 @@ memory/L4_raw_sessions/*
|
||||
# TMWebDriver SOP
|
||||
!memory/tmwebdriver_sop.md
|
||||
|
||||
# Vue3 Component SOP
|
||||
!memory/vue3_component_sop.md
|
||||
|
||||
# Subagent SOP
|
||||
!memory/subagent_sop.md
|
||||
|
||||
@@ -68,6 +71,9 @@ memory/L4_raw_sessions/*
|
||||
# Plan SOP
|
||||
!memory/plan_sop.md
|
||||
|
||||
# Goal Mode SOP
|
||||
!memory/goal_mode_sop.md
|
||||
|
||||
# Skill Search SOP
|
||||
!memory/skill_search/
|
||||
!memory/skill_search/**
|
||||
@@ -103,6 +109,7 @@ reflect/*
|
||||
!reflect/autonomous.py
|
||||
!reflect/scheduler.py
|
||||
!reflect/agent_team_worker.py
|
||||
!reflect/goal_mode.py
|
||||
|
||||
# Universal: never track __pycache__ anywhere
|
||||
**/__pycache__/
|
||||
|
||||
@@ -234,6 +234,7 @@ if __name__ == '__main__':
|
||||
try: task = mod.check()
|
||||
except Exception as e:
|
||||
print(f'[Reflect] check() error: {e}'); continue
|
||||
if task and task == '/exit': break
|
||||
if task is None: continue
|
||||
print(f'[Reflect] triggered: {task[:80]}')
|
||||
dq = agent.put_task(task, source='reflect')
|
||||
|
||||
@@ -9,7 +9,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
from agent_loop import BaseHandler, StepOutcome, json_default
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def code_run(code, code_type="python", timeout=60, cwd=None, code_cwd=None, stop_signal=[]):
|
||||
def code_run(code, code_type="python", timeout=60, cwd=None, code_cwd=None, stop_signal=None):
|
||||
"""代码执行器
|
||||
python: 运行复杂的 .py 脚本(文件模式)
|
||||
powershell/bash: 运行单行指令(命令模式)
|
||||
@@ -59,7 +59,7 @@ def code_run(code, code_type="python", timeout=60, cwd=None, code_cwd=None, stop
|
||||
|
||||
while t.is_alive():
|
||||
istimeout = time.time() - start_t > timeout
|
||||
if istimeout or len(stop_signal) > 0:
|
||||
if istimeout or stop_signal:
|
||||
process.kill()
|
||||
print("[Debug] Process killed due to timeout or stop signal.")
|
||||
if istimeout: full_stdout.append("\n[Timeout Error] 超时强制终止")
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Goal Mode SOP
|
||||
|
||||
## 何时使用
|
||||
|
||||
用户给出开放目标 + 时间预算(如"花3小时持续优化X"、"没事也找事干"),且不是一次性闭环任务。
|
||||
|
||||
## 设置
|
||||
|
||||
写 `temp/goal_state.json`(或自定义路径):
|
||||
|
||||
```json
|
||||
{
|
||||
"objective": "用户原话目标",
|
||||
"budget_seconds": 10800,
|
||||
"start_time": <time.time()>,
|
||||
"turns_used": 0,
|
||||
"max_turns": 200,
|
||||
"status": "running"
|
||||
}
|
||||
```
|
||||
|
||||
- `budget_seconds`:最少 3 小时(10800),按用户要求调整
|
||||
- `max_turns`:防空转上限,一般 200 够用
|
||||
- `status`:必须为 `"running"`
|
||||
|
||||
## 启动
|
||||
|
||||
必须后台启动(长时间运行,不占前台终端):
|
||||
|
||||
```bash
|
||||
# 默认路径 temp/goal_state.json
|
||||
start /b python agentmain.py --reflect reflect/goal_mode.py
|
||||
|
||||
# 自定义路径(多实例)
|
||||
set GOAL_STATE=temp/goal_xxx.json && start /b python agentmain.py --reflect reflect/goal_mode.py
|
||||
|
||||
# 用其他模型跑(--llm_no 选择已配置的第N个LLM,从0开始)
|
||||
set GOAL_STATE=temp/goal_xxx.json && start /b python agentmain.py --reflect reflect/goal_mode.py --llm_no 1
|
||||
```
|
||||
|
||||
## 停止
|
||||
|
||||
- 预算耗尽时自动进入收口轮,然后停止
|
||||
- 手动停:杀进程
|
||||
|
||||
## 观察进度
|
||||
|
||||
- 状态:读 goal_state.json 的 `turns_used` / `status`
|
||||
- 详情:看 `temp/model_responses/` 下最近修改的文件尾部
|
||||
@@ -0,0 +1,163 @@
|
||||
# Vue 3 自定义组件 JS 操作 SOP
|
||||
|
||||
## 问题
|
||||
Vue 3 自定义组件(如 OxdSelect)通过 `addEventListener` 绑定事件,JS `dispatchEvent` 产生的事件 `isTrusted: false`,组件不响应。
|
||||
- `element.click()` 无效(组件可能绑定 mousedown 而非 click)
|
||||
- `dispatchEvent(new MouseEvent('mousedown'))` 无效(isTrusted:false)
|
||||
- `element.focus()` 不触发 Vue 绑定的 focus handler
|
||||
|
||||
## 解决方案:直接操作 Vue 组件实例
|
||||
|
||||
### 1. 获取 Vue 3 根入口
|
||||
```javascript
|
||||
const rootVnode = document.getElementById('app')._vnode;
|
||||
```
|
||||
|
||||
### 2. 遍历 vnode 树匹配 DOM 元素
|
||||
```javascript
|
||||
function findCompByEl(vnode, targetEl, depth = 0) {
|
||||
if (depth > 50 || !vnode) return null;
|
||||
const comp = vnode.component;
|
||||
if (comp) {
|
||||
if (comp.vnode?.el === targetEl || comp.subTree?.el === targetEl) return comp;
|
||||
if (comp.vnode?.el?.contains?.(targetEl)) {
|
||||
const result = findCompByEl(comp.subTree, targetEl, depth + 1);
|
||||
if (result) return result;
|
||||
return comp;
|
||||
}
|
||||
const subResult = findCompByEl(comp.subTree, targetEl, depth + 1);
|
||||
if (subResult) return subResult;
|
||||
}
|
||||
if (vnode.children && Array.isArray(vnode.children)) {
|
||||
for (const child of vnode.children) {
|
||||
const result = findCompByEl(child, targetEl, depth + 1);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
if (vnode.dynamicChildren) {
|
||||
for (const child of vnode.dynamicChildren) {
|
||||
const result = findCompByEl(child, targetEl, depth + 1);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 调用组件方法
|
||||
```javascript
|
||||
// 目标DOM的parentElement通常是组件根元素
|
||||
const comp = findCompByEl(rootVnode, targetElement.parentElement);
|
||||
const ctx = comp.proxy;
|
||||
|
||||
// 查看可用方法
|
||||
Object.keys(ctx).filter(k => !k.startsWith('_') && !k.startsWith('$'));
|
||||
|
||||
// Select 类组件:直接调用 onSelect
|
||||
ctx.onSelect({id: 'USD', label: 'United States Dollar'});
|
||||
|
||||
// 获取选项列表
|
||||
ctx.computedOptions; // [{id, label, _selected}, ...]
|
||||
```
|
||||
|
||||
## 组件层级注意
|
||||
- **展示层**(如 OxdSelectText):只有 onToggle/onFocus/onBlur,调用无实际效果
|
||||
- **逻辑层**(如 OxdSelectInput,是展示层的父组件):有 openDropdown/onSelect/computedOptions/onCloseDropdown
|
||||
- 定位逻辑层:用 `targetElement.parentElement` 而非 targetElement 本身
|
||||
|
||||
### 弹窗内 Select 同样纯 JS 优先(已验证)
|
||||
- 弹窗(`.oxd-dialog-sheet`)内的 `.oxd-select-text` 用循环向上查找同样能命中 `OxdSelectInput`,`onSelect` 正常工作。
|
||||
- 不需要 CDP 兜底。仅当循环 8 层仍找不到组件时才考虑 CDP 打开+JS 点 option。
|
||||
|
||||
### 循环向上查找模式(推荐)
|
||||
单层 `parentElement` 可能不够,用循环更健壮:
|
||||
```javascript
|
||||
function findSelectComp(selectTextEl) {
|
||||
for (let el = selectTextEl, up = 0; el && up < 8; el = el.parentElement, up++) {
|
||||
const comp = findCompByEl(rootVnode, el);
|
||||
if (comp?.proxy?.onSelect && comp.proxy.computedOptions?.length) return comp;
|
||||
}
|
||||
return null; // 找不到再考虑CDP兜底
|
||||
}
|
||||
```
|
||||
|
||||
## 普通 Input/Textarea 操作(nativeSetter)
|
||||
|
||||
Vue 3 的 `v-model` 监听 input 事件,直接 `el.value = x` 不触发响应式。需用原型 setter:
|
||||
|
||||
```javascript
|
||||
// Input
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
||||
setter.call(inputEl, '新值');
|
||||
inputEl.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
inputEl.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
|
||||
// Textarea
|
||||
const taSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
|
||||
taSetter.call(textareaEl, '内容');
|
||||
textareaEl.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
```
|
||||
|
||||
### Date Input 特殊处理
|
||||
日期组件通常有 blur 校验,需要 focus→赋值→blur 完整链:
|
||||
```javascript
|
||||
dateInput.focus();
|
||||
setter.call(dateInput, '2026-08-05');
|
||||
dateInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
dateInput.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
dateInput.dispatchEvent(new Event('blur', {bubbles: true}));
|
||||
```
|
||||
|
||||
### Button
|
||||
普通 `.click()` 即可,Vue 3 不检查 button click 的 isTrusted。
|
||||
|
||||
### File Upload (input[type="file"])
|
||||
浏览器安全模型禁止JS直接 `input.value='path'`,但可用 DataTransfer API 构造 FileList:
|
||||
```javascript
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const content = '文件内容';
|
||||
const file = new File([content], 'filename.txt', { type: 'text/plain', lastModified: Date.now() });
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
fileInput.files = dt.files; // Chrome 62+ 支持
|
||||
fileInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
```
|
||||
- 适用于任何框架(非Vue3特有),纯浏览器API
|
||||
- 可构造任意类型文件(Blob/ArrayBuffer均可传入File构造器)
|
||||
- ⚠ CDP `DOM.setFileInputFiles` 只设files属性不触发事件(Chrome通用行为),DataTransfer+dispatch是唯一纯JS方案
|
||||
- ⚠ 确保弹窗/容器已打开再querySelector,否则input不在DOM中
|
||||
|
||||
## 泛化到其他 Vue3 站点(未逐一验证,思路层面)
|
||||
|
||||
本 SOP 的核心方法(根 vnode → findCompByEl → proxy)是 Vue3 通用的,但具体方法名/属性名因 UI 库而异。
|
||||
|
||||
面对陌生 Vue3 站点的探测思路:
|
||||
|
||||
1. **确认是 Vue3** — `document.getElementById('app')?.__vue_app__` 存在即可
|
||||
2. **定位目标 DOM** — 用选择器找到要操作的元素(如某个 select wrapper)
|
||||
3. **从 DOM 反查组件** — 用 findCompByEl 从目标元素及其父级向上找,拿到 component
|
||||
4. **探测组件能力** — 拿到 comp 后查看:
|
||||
- `Object.keys(comp.proxy.$options.methods || {})` → 组件方法名
|
||||
- `Object.keys(comp.props || {})` → props
|
||||
- `Object.keys(comp.setupState || {})` → setup 暴露的响应式数据和函数
|
||||
- 重点找类似 onSelect/handleSelect/select/setValue 的方法,以及 options/items/computedOptions 之类的选项列表
|
||||
5. **试调** — 找到疑似选中方法后,传入选项对象试调,观察 DOM 是否更新
|
||||
6. **选项格式** — 不同库的 option 结构不同(可能是 `{id, label}` 也可能是 `{value, text}` 或纯字符串),从选项列表数据中取一个完整对象传入即可
|
||||
|
||||
注意事项:
|
||||
- 有些库用 `emits` 而非 methods,选中逻辑可能在父组件而非子组件
|
||||
- 有些库 prod build 会 minify 方法名,此时 setupState 里的 key 可能是短名,需结合行为猜测
|
||||
- Composition API 组件的逻辑主要在 setupState 而非 $options.methods
|
||||
- 如果 proxy 上找不到方法,试试 `comp.exposed`(`<script setup>` 用 defineExpose 暴露的)
|
||||
|
||||
## 适用场景
|
||||
- Vue 3 自定义 Select/Dropdown/Autocomplete 组件 → vnode 实例方法
|
||||
- Vue 3 普通 Input/Textarea(v-model)→ nativeSetter + input 事件
|
||||
- Date 组件 → nativeSetter + focus/blur 链
|
||||
- File Upload → DataTransfer + change 事件
|
||||
- 需要绕过 isTrusted 检查的场景
|
||||
|
||||
## 验证于
|
||||
- OrangeHRM (opensource-demo.orangehrmlive.com) Vue 3 + OXD 组件库
|
||||
- 2026-05-08
|
||||
@@ -0,0 +1,95 @@
|
||||
# reflect/goal_mode.py — Goal Mode: 持续自驱直到预算耗尽
|
||||
# 启动: set GOAL_STATE=temp/xxx.json && python agentmain.py --reflect reflect/goal_mode.py
|
||||
# 配置: agent按SOP写好state json,通过环境变量GOAL_STATE指定路径
|
||||
import os, json, time
|
||||
|
||||
INTERVAL = 3 # check间隔短,agent跑完立刻再检查
|
||||
ONCE = False
|
||||
|
||||
_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
STATE_FILE = os.environ.get('GOAL_STATE') or os.path.join(_dir, '../temp/goal_state.json')
|
||||
if not os.path.isabs(STATE_FILE):
|
||||
STATE_FILE = os.path.join(_dir, '..', STATE_FILE)
|
||||
|
||||
# --- state 管理 ---
|
||||
def _load():
|
||||
if not os.path.isfile(STATE_FILE): return None
|
||||
with open(STATE_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def _save(state):
|
||||
with open(STATE_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(state, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# --- prompt 模板 ---
|
||||
CONTINUATION_PROMPT = """[Goal Mode — 持续推进]
|
||||
|
||||
<untrusted_objective>
|
||||
{objective}
|
||||
</untrusted_objective>
|
||||
|
||||
⏱ 已用 {elapsed_min:.0f} 分钟,剩余约 {remaining_min:.0f} 分钟。第 {turn} 次唤醒。
|
||||
|
||||
你正在 Goal Mode 下工作:
|
||||
1. 禁止说"已完成,是否继续"——预算没到就不准停。
|
||||
2. 在 cwd 下建立工作文件夹存放成果和进度,复杂任务可使用 plan 模式。
|
||||
3. 如果当前方向做完了,主动找下一个改进点:测试/边界case/性能/安全/文档/代码质量。
|
||||
4. 找不到改进点?扩大视野:关联模块、上下游依赖、用户体验、错误提示、日志可观测性、上网搜索、找其他路径、翻记忆里面有无相关。
|
||||
"""
|
||||
|
||||
BUDGET_LIMIT_PROMPT = """[Goal Mode — 预算耗尽,收口]
|
||||
|
||||
<untrusted_objective>
|
||||
{objective}
|
||||
</untrusted_objective>
|
||||
|
||||
⏱ 预算已耗尽({budget_min:.0f} 分钟)。这是最后一轮。
|
||||
|
||||
请执行收口:
|
||||
1. 总结本次 goal 的所有进展(列表)。
|
||||
2. 列出未完成的事项和建议的 next step。
|
||||
3. 确保工作文件夹中记录了关键成果,以便下次继续。
|
||||
"""
|
||||
|
||||
# --- 主逻辑 ---
|
||||
def check():
|
||||
state = _load()
|
||||
if state is None: return '/exit'
|
||||
|
||||
status = state.get('status', 'running')
|
||||
if status != 'running': return '/exit'
|
||||
|
||||
start_time = state.get('start_time', time.time())
|
||||
budget_sec = state.get('budget_seconds', 1800) # 默认30分钟
|
||||
elapsed = time.time() - start_time
|
||||
remaining = budget_sec - elapsed
|
||||
turn = state.get('turns_used', 0) + 1
|
||||
max_turns = state.get('max_turns', 50) # 防空转上限
|
||||
|
||||
# 预算耗尽或轮次上限
|
||||
if remaining <= 0 or turn > max_turns:
|
||||
state['status'] = 'wrapping_up'
|
||||
_save(state)
|
||||
return BUDGET_LIMIT_PROMPT.format(
|
||||
objective=state['objective'],
|
||||
budget_min=budget_sec / 60
|
||||
)
|
||||
|
||||
# 正常continuation
|
||||
state['turns_used'] = turn
|
||||
_save(state)
|
||||
return CONTINUATION_PROMPT.format(
|
||||
objective=state['objective'],
|
||||
elapsed_min=elapsed / 60,
|
||||
remaining_min=remaining / 60,
|
||||
turn=turn
|
||||
)
|
||||
|
||||
def on_done(result):
|
||||
state = _load()
|
||||
if state is None: return
|
||||
|
||||
if state.get('status') == 'wrapping_up':
|
||||
state['status'] = 'done_budget'
|
||||
state['end_time'] = time.time()
|
||||
_save(state)
|
||||
Reference in new issue
Block a user