diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index e7be14a..1be9abd 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## [0.5.17] - 2026-04-25 + +### 修复 +- **[admin-frontend]**: 修复节点编辑 / 批量修改保存权限组后订阅侧无法命中节点的问题;前端提交 `group_ids / route_ids` 时统一序列化为字符串 ID,后端 `whereGroupId` 同时兼容历史字符串与数字 JSON 值,并补齐 TUIC V5/V4、ALPN 选项与 AnyTLS 完整默认 Padding Scheme — by yinjianm + - 类型: 快速修改(无方案包) + - 文件: admin-frontend/src/utils/nodeEditorMapper.ts, admin-frontend/src/utils/nodeEditorOptions.ts, admin-frontend/src/views/nodes/NodeEditorProtocolSection.vue, app/Models/Server.php + ## [0.5.16] - 2026-04-25 ### 新增 diff --git a/.helloagents/modules/admin-frontend.md b/.helloagents/modules/admin-frontend.md index c24fc15..7720a09 100644 --- a/.helloagents/modules/admin-frontend.md +++ b/.helloagents/modules/admin-frontend.md @@ -36,6 +36,8 @@ - 用户流量重置优先复用 `traffic-reset/reset-user`,用户行级“重置流量”会走真实后端重置链路并在成功后刷新列表 - 节点管理页通过真实后端 `server/manage/getNodes`、`server/group/fetch` 与 `server/route/fetch` 获取列表 / 关联数据,并通过 `server/manage/save`、`server/manage/sort`、`server/manage/update`、`server/manage/batchUpdate`、`server/manage/copy`、`server/manage/drop` 完成新增、编辑、排序、批量修改与行级操作 - 节点新增 / 编辑采用统一中央大弹窗,支持 `Shadowsocks / VMess / Trojan / Hysteria / VLess / TUIC / SOCKS / Naive / HTTP / Mieru / AnyTLS` 11 种协议的首版动态配置表单 +- 节点新增 / 编辑 / 批量修改保存 `group_ids / route_ids` 时统一向后端提交字符串 ID,后端 `Server::whereGroupId()` 同时兼容历史字符串与数字 JSON 值,避免权限组保存后订阅侧无法命中节点 +- TUIC 表单默认以 V5 / V4 版本选择、`h3 / h2 / http/1.1` ALPN 选项和 `native / quic` UDP Relay Mode 对齐后端协议模板;AnyTLS Padding Scheme 默认值与 `Server` 模型完整模板保持一致 - 节点排序采用本地草稿 + 上移 / 下移模式,保存时向 `server/manage/sort` 提交 `{ id, order }[]` 顺序 payload - 节点列表现支持本地分页、在线 / 离线筛选、父/子节点筛选,以及跨分页稳定勾选;批量修改 / 批量删除仅作用于已勾选节点,其中批量修改可统一更新 `host / group_ids / rate` - 节点行级菜单现已补齐“置顶节点”,会复用当前排序结果生成新的顺序 payload 并提交到 `server/manage/sort` diff --git a/.helloagents/sessions/master/default/STATE.md b/.helloagents/sessions/master/default/STATE.md index 9e7a78f..557dbd9 100644 --- a/.helloagents/sessions/master/default/STATE.md +++ b/.helloagents/sessions/master/default/STATE.md @@ -1,25 +1,28 @@ # 恢复快照 ## 主线目标 -为 `admin-frontend` 用户管理高级筛选新增“活跃状态”条件,并补齐对应后端复合过滤规则。 +补充 HelloAGENTS 通用避坑指南中的“多个任务并行执行”处理规则。 ## 正在做什么 -当前任务已完成,已补齐活跃 / 非活跃筛选与前后端联动,并完成前端构建验证。 +当前任务已完成,通用文档已补充“多任务并行执行规则”。 ## 关键上下文 -- 高级筛选弹窗新增了 `activity_status` 字段,前端支持选择“活跃 / 非活跃”,默认无该条件即代表“全部”。 -- 后端 `UserController::fetch()` 现支持 `activity_status=eq:1|0` 的复合规则:`plan_id` 非空、剩余流量大于 0、`last_online_at` 在近半年内即视为活跃。 -- 已新增 `tests/Unit/Admin/UserControllerActivityStatusFilterTest.php` 覆盖值解析与 SQL 条件拼装,但当前环境缺少可执行 `php` 命令,尚未本机跑通该 PHPUnit 用例。 -- 已完成 `admin-frontend` 的 `npm run build`,最新产物已写入 `public/assets/admin` 子模块。 +- 已有项目版文档:`docs/helloagents-workflow-pitfalls.md`。 +- 通用版目标路径:`docs/helloagents-universal-pitfalls.md`。 +- 通用版面向任意已激活 HelloAGENTS 项目,重点覆盖状态恢复、输出格式、目录边界、验证证据、子仓/子模块、多平台 Shell、外部依赖和收尾流程。 +- 文档不放入 `.helloagents/`,避免触发模板格式约束。 +- 本次新增重点:并行任务必须先建任务矩阵;主代理处理关键路径,子代理仅在用户明确授权并行 / 委托时使用;并行写入必须分配不重叠文件范围;状态文件记录并行泳道和合并点。 +- 已在 `docs/helloagents-universal-pitfalls.md` 新增 `## 多任务并行执行规则` 章节。 +- 已检查章节存在性与 `git diff --check`,无空白错误;仅提示 Git 未来可能将 `STATE.md` LF 替换为 CRLF。 ## 下一步 -当前任务已完成;如继续同一业务域,建议在具备 PHP 运行时的环境补跑 `UserControllerActivityStatusFilterTest`,并用真实后台登录态手动验证“高级筛选 → 活跃 / 非活跃切换”的结果集。 +当前补充已完成;后续可把该章节抽成团队并行任务模板。 ## 阻塞项 -- 当前终端不存在 `php` +- 暂无 ## 方案 -`.helloagents/archive/2026-04/202604250018_admin-frontend-user-activity-status-filter/` +快速文档沉淀(无独立方案包)。 ## 已标记技能 hello-ui, hello-verify diff --git a/admin-frontend/src/types/api.d.ts b/admin-frontend/src/types/api.d.ts index f82239b..2d9f05d 100644 --- a/admin-frontend/src/types/api.d.ts +++ b/admin-frontend/src/types/api.d.ts @@ -909,7 +909,7 @@ export interface AdminNodeBatchUpdatePayload { ids: number[] host?: string rate?: number - group_ids?: number[] + group_ids?: string[] } export interface AdminNodeSavePayload { @@ -917,8 +917,8 @@ export interface AdminNodeSavePayload { type: AdminNodeType code?: string name: string - group_ids?: number[] - route_ids?: number[] + group_ids?: string[] + route_ids?: string[] parent_id?: number | null enabled?: boolean host: string diff --git a/admin-frontend/src/utils/nodeEditorMapper.ts b/admin-frontend/src/utils/nodeEditorMapper.ts index 049c54a..5e54f68 100644 --- a/admin-frontend/src/utils/nodeEditorMapper.ts +++ b/admin-frontend/src/utils/nodeEditorMapper.ts @@ -55,6 +55,14 @@ function toNumberArray(value: unknown): number[] { )] } +function toJsonIdArray(value: unknown[]): string[] { + return [...new Set( + value + .map((item) => String(item ?? '').trim()) + .filter(Boolean), + )] +} + function splitMultiline(value: string): string[] { return [...new Set( value @@ -154,6 +162,9 @@ export function toNodeFormModel(node?: AdminNodeItem | null): NodeFormModel { : createEmptyNodeForm().rateTimeRanges form.tags = toStringArray(node.tags) form.groupIds = toNumberArray(node.group_ids) + if (form.groupIds.length === 0 && Array.isArray(node.groups)) { + form.groupIds = toNumberArray(node.groups.map((group) => group.id)) + } form.routeIds = toNumberArray(node.route_ids) form.host = toStringValue(node.host) form.port = toStringValue(node.port) @@ -467,8 +478,8 @@ export function toNodeSavePayload(form: NodeFormModel): AdminNodeSavePayload { type: form.type as AdminNodeType, code: form.code.trim() || undefined, name: form.name.trim(), - group_ids: [...new Set(form.groupIds)], - route_ids: [...new Set(form.routeIds)], + group_ids: toJsonIdArray(form.groupIds), + route_ids: toJsonIdArray(form.routeIds), parent_id: form.parentId ?? undefined, enabled: form.enabled, host: form.host.trim(), diff --git a/admin-frontend/src/utils/nodeEditorOptions.ts b/admin-frontend/src/utils/nodeEditorOptions.ts index 85a80fc..9966adb 100644 --- a/admin-frontend/src/utils/nodeEditorOptions.ts +++ b/admin-frontend/src/utils/nodeEditorOptions.ts @@ -197,6 +197,17 @@ export const NODE_UDP_RELAY_MODE_OPTIONS: Array = [ { value: 'quic', label: 'quic' }, ] +export const NODE_TUIC_VERSION_OPTIONS: Array> = [ + { value: 5, label: 'V5' }, + { value: 4, label: 'V4' }, +] + +export const NODE_ALPN_OPTIONS: Array = [ + { value: 'h3', label: 'HTTP/3' }, + { value: 'h2', label: 'HTTP/2' }, + { value: 'http/1.1', label: 'HTTP/1.1' }, +] + export const NODE_MUX_PROTOCOL_OPTIONS: Array = [ { value: 'yamux', label: 'yamux' }, { value: 'smux', label: 'smux' }, @@ -290,6 +301,11 @@ export function createEmptyNodeForm(): NodeFormModel { '0=30-30', '1=100-400', '2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000', + '3=9-9,500-1000', + '4=500-1000', + '5=500-1000', + '6=500-1000', + '7=500-1000', ].join('\n'), multiplexEnabled: false, multiplexProtocol: 'yamux', diff --git a/admin-frontend/src/views/nodes/NodeBatchEditDialog.vue b/admin-frontend/src/views/nodes/NodeBatchEditDialog.vue index a5204d2..d64c599 100644 --- a/admin-frontend/src/views/nodes/NodeBatchEditDialog.vue +++ b/admin-frontend/src/views/nodes/NodeBatchEditDialog.vue @@ -6,7 +6,7 @@ import type { AdminServerGroupItem } from '@/types/api' interface NodeBatchEditPayload { host?: string rate?: number - group_ids?: number[] + group_ids?: string[] } const props = defineProps<{ @@ -64,7 +64,7 @@ function handleSubmit() { emit('submit', { host: form.updateHost ? form.host.trim() : undefined, rate: form.updateRate ? Number(form.rate) : undefined, - group_ids: form.updateGroups ? [...form.groupIds] : undefined, + group_ids: form.updateGroups ? [...new Set(form.groupIds.map((item) => String(item)))] : undefined, }) } diff --git a/admin-frontend/src/views/nodes/NodeEditorProtocolSection.vue b/admin-frontend/src/views/nodes/NodeEditorProtocolSection.vue index 5a674bc..0b98816 100644 --- a/admin-frontend/src/views/nodes/NodeEditorProtocolSection.vue +++ b/admin-frontend/src/views/nodes/NodeEditorProtocolSection.vue @@ -2,12 +2,14 @@ import { computed } from 'vue' import { getNodeProtocolHint, + NODE_ALPN_OPTIONS, NODE_CONGESTION_CONTROL_OPTIONS, NODE_MUX_PROTOCOL_OPTIONS, NODE_SHADOWSOCKS_CIPHER_OPTIONS, NODE_SHADOWSOCKS_OBFS_OPTIONS, NODE_TCP_HEADER_OPTIONS, NODE_TLS_FINGERPRINT_OPTIONS, + NODE_TUIC_VERSION_OPTIONS, NODE_UDP_RELAY_MODE_OPTIONS, NODE_VLESS_FLOW_OPTIONS, shouldShowRealitySettings, @@ -58,13 +60,6 @@ const currentProtocolHint = computed(() => getNodeProtocolHint(props.form.type)) - - - - @@ -366,7 +361,14 @@ const currentProtocolHint = computed(() => getNodeProtocolHint(props.form.type)) diff --git a/app/Console/Commands/CheckTrafficExceeded.php b/app/Console/Commands/CheckTrafficExceeded.php index 35a1db6..0a85600 100644 --- a/app/Console/Commands/CheckTrafficExceeded.php +++ b/app/Console/Commands/CheckTrafficExceeded.php @@ -43,7 +43,7 @@ class CheckTrafficExceeded extends Command } $userIdsInGroup = $users->pluck('id')->toArray(); - $servers = Server::whereJsonContains('group_ids', (string) $groupId)->get(); + $servers = Server::whereGroupId($groupId)->get(); foreach ($servers as $server) { if (!NodeSyncService::isNodeOnline($server->id)) { diff --git a/app/Http/Controllers/V2/Admin/Server/GroupController.php b/app/Http/Controllers/V2/Admin/Server/GroupController.php index 83a53ac..a515a71 100644 --- a/app/Http/Controllers/V2/Admin/Server/GroupController.php +++ b/app/Http/Controllers/V2/Admin/Server/GroupController.php @@ -51,7 +51,7 @@ class GroupController extends Controller if (!$serverGroup) { return $this->fail([400202, '组不存在']); } - if (Server::whereJsonContains('group_ids', $groupId)->exists()) { + if (Server::whereGroupId($groupId)->exists()) { return $this->fail([400, '该组已被节点所使用,无法删除']); } diff --git a/app/Models/Server.php b/app/Models/Server.php index c65754d..e00dec5 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -135,6 +135,46 @@ class Server extends Model 'machine_id' => 'integer', ]; + private function normalizeJsonIdList($value): array + { + if (is_string($value)) { + $decoded = json_decode($value, true); + $value = json_last_error() === JSON_ERROR_NONE ? $decoded : [$value]; + } + + if (!is_array($value)) { + return []; + } + + return array_values(array_unique(array_filter(array_map( + fn ($item) => trim((string) $item), + $value + ), fn ($item) => $item !== ''))); + } + + public function setGroupIdsAttribute($value): void + { + $this->attributes['group_ids'] = json_encode($this->normalizeJsonIdList($value)); + } + + public function setRouteIdsAttribute($value): void + { + $this->attributes['route_ids'] = json_encode($this->normalizeJsonIdList($value)); + } + + public function scopeWhereGroupId($query, $groupId) + { + $normalized = trim((string) $groupId); + + return $query->where(function ($query) use ($normalized) { + $query->whereJsonContains('group_ids', $normalized); + + if (ctype_digit($normalized)) { + $query->orWhereJsonContains('group_ids', (int) $normalized); + } + }); + } + private const MULTIPLEX_CONFIGURATION = [ 'multiplex' => [ 'type' => 'object', diff --git a/app/Models/ServerGroup.php b/app/Models/ServerGroup.php index 57f3514..63c4fbf 100755 --- a/app/Models/ServerGroup.php +++ b/app/Models/ServerGroup.php @@ -31,7 +31,7 @@ class ServerGroup extends Model public function servers() { - return Server::whereJsonContains('group_ids', (string) $this->id)->get(); + return Server::whereGroupId($this->id)->get(); } /** @@ -40,7 +40,7 @@ class ServerGroup extends Model protected function serverCount(): Attribute { return Attribute::make( - get: fn () => Server::whereJsonContains('group_ids', (string) $this->id)->count(), + get: fn () => Server::whereGroupId($this->id)->count(), ); } } diff --git a/app/Services/NodeSyncService.php b/app/Services/NodeSyncService.php index 6d714e1..d012e4c 100644 --- a/app/Services/NodeSyncService.php +++ b/app/Services/NodeSyncService.php @@ -39,7 +39,7 @@ class NodeSyncService */ public static function notifyUsersUpdatedByGroup(int $groupId): void { - $servers = Server::whereJsonContains('group_ids', (string) $groupId) + $servers = Server::whereGroupId($groupId) ->get(); foreach ($servers as $server) { @@ -59,7 +59,7 @@ class NodeSyncService if (!$user->group_id) return; - $servers = Server::whereJsonContains('group_ids', (string) $user->group_id)->get(); + $servers = Server::whereGroupId($user->group_id)->get(); foreach ($servers as $server) { if (!self::isNodeOnline($server->id)) continue; @@ -90,7 +90,7 @@ class NodeSyncService */ public static function notifyUserRemovedFromGroup(int $userId, int $groupId): void { - $servers = Server::whereJsonContains('group_ids', (string) $groupId) + $servers = Server::whereGroupId($groupId) ->get(); foreach ($servers as $server) { diff --git a/app/Services/ServerService.php b/app/Services/ServerService.php index 19a5998..579a843 100644 --- a/app/Services/ServerService.php +++ b/app/Services/ServerService.php @@ -54,7 +54,7 @@ class ServerService */ public static function getAvailableServers(User $user): array { - $servers = Server::whereJsonContains('group_ids', (string) $user->group_id) + $servers = Server::whereGroupId($user->group_id) ->where('show', true) ->where(function ($query) { $query->whereNull('transfer_enable') diff --git a/docs/helloagents-universal-pitfalls.md b/docs/helloagents-universal-pitfalls.md new file mode 100644 index 0000000..031257e --- /dev/null +++ b/docs/helloagents-universal-pitfalls.md @@ -0,0 +1,633 @@ +# HelloAGENTS 通用避坑指南 + +## 适用范围 + +本文面向所有使用 HelloAGENTS Standby 工作流的项目,尤其适用于: + +- 已存在 `.helloagents/` 的激活项目。 +- 需要在同一轮中完成编码、验证、知识记录和收尾的任务。 +- Windows、Linux、macOS 混合协作环境。 +- 包含子仓、子模块、构建产物目录或多分支发布链路的项目。 +- 用户希望“直接做完”,但项目内存在流程状态、模板和验证约束的场景。 + +本文不绑定任何具体业务项目。若项目已有更深层规则,应以系统 / 开发者 / 用户直接指令和项目内更具体规则为准。 + +## 一句话原则 + +先认当前任务,再读旧状态;先看代码证据,再写结论;先完成必要验证,再收尾;工具失败必须显式记录。 + +## 总体优先级 + +执行任何任务前,按以下顺序判断当前真实状态: + +1. 用户最新消息和明确授权。 +2. 本轮已确认的范围、目标和约束。 +3. 当前代码、配置、命令输出和真实验证证据。 +4. 活跃方案包、任务清单和验收契约。 +5. `.helloagents/` 状态文件与历史知识。 +6. 记忆、旧聊天记录和经验判断。 + +不要让旧状态、旧方案或记忆覆盖当前用户的明确需求。 + +## 常见坑点与规避规则 + +### 1. 旧状态文件误导当前任务 + +**坑点** + +`.helloagents/sessions/.../STATE.md` 可能记录上一个任务。用户新开任务时,如果直接按旧状态继续,会进入错误业务线。 + +**规避** + +- 先判断用户最新消息是否是新任务。 +- 如果是新任务,立即重写 `STATE.md`。 +- 旧状态只能用于恢复上下文,不能替代当前需求。 + +**检查句** + +> 当前用户是在继续旧任务,还是提出了一个新任务? + +### 2. 把状态文件当成自动授权 + +**坑点** + +状态文件可能写着“下一步部署”“下一步发布”,但这不等于用户本轮授权了外部副作用。 + +**规避** + +- 本地读写、构建、测试通常可直接执行。 +- 推送、发布、生产数据库、线上配置、付费资源等外部副作用必须确认是否已授权。 +- 状态文件只能说明“上次做到哪里”,不能替代风险确认。 + +**检查句** + +> 这个动作会不会影响远端、生产、账单、用户数据或不可逆状态? + +### 3. `output_format=true` 误用于中间消息 + +**坑点** + +HelloAGENTS 输出格式只适合最终收尾。中间进度、工具前说明、阻塞排查如果套用最终格式,容易让运行时误判完成或等待输入。 + +**规避** + +- 中间过程使用自然语言或计划工具。 +- 最后一条消息才使用 HelloAGENTS 外层格式。 +- 只在确定不再调用工具、不再继续执行时收尾。 + +**检查句** + +> 发出这条消息后,我还会继续调用工具吗? + +如果会,就不要使用最终输出格式。 + +### 4. 收尾状态脚本不存在或不可用 + +**坑点** + +协议可能要求执行 `scripts/turn-state.mjs write`,但具体项目未必存在该脚本。 + +**规避** + +- 收尾前按协议尝试一次。 +- 如果脚本缺失或执行失败,明确记录错误。 +- 不要假装写入成功。 +- 同步更新项目 `STATE.md` 作为恢复依据。 + +**推荐记录** + +```text +turn-state 脚本缺失,无法写入运行态信号;已更新 STATE.md 作为恢复状态。 +``` + +### 5. `.helloagents/` 目录被当成普通目录 + +**坑点** + +`.helloagents/` 通常有固定模板、状态文件、方案包和知识库约束。随意新增普通文档,可能破坏流程结构。 + +**规避** + +- 普通交付文档优先放到 `docs/`。 +- 只有状态、方案、知识库、归档等 HelloAGENTS 定义产物才放进 `.helloagents/`。 +- 更新 `.helloagents/` 内文件时,遵循既有模板和格式。 + +**检查句** + +> 这个文件是流程产物,还是普通交付物? + +普通交付物不要默认放进 `.helloagents/`。 + +### 6. 小修小补被膨胀成完整方案包 + +**坑点** + +项目已激活不代表每个任务都要创建完整计划包。明确的小范围修复如果过度流程化,会拖慢执行。 + +**规避** + +可直接按快速修改处理的情况: + +- 用户给出具体故障、页面、文件或报错。 +- 修改范围可限定在少量文件。 +- 不涉及生产高风险操作。 +- 可以通过本地命令或静态检查验证。 + +需要方案包的情况: + +- 多模块新功能。 +- 大范围 UI/架构改造。 +- 需求存在重要歧义。 +- 高风险或不可逆操作。 + +**检查句** + +> 这是明确 bugfix,还是需要设计决策的新能力? + +### 7. 记忆和历史经验被当成当前事实 + +**坑点** + +记忆可以提示历史坑,但可能已经过期。直接基于记忆下结论,会错过当前分支、依赖、目录结构和配置变化。 + +**规避** + +- 用记忆快速定位重点。 +- 用当前命令验证事实。 +- 最终回复中区分“当前已验证”和“基于历史经验判断”。 + +**检查句** + +> 这个结论来自当前命令,还是来自历史记忆? + +### 8. 子仓、子模块和构建产物被误判 + +**坑点** + +根仓只显示一个子模块指针变化时,用户可能看不到真实文件差异。代理如果只看根仓,也可能误以为没改到。 + +**规避** + +涉及子仓 / 子模块时,必须双层取证: + +1. 根仓状态。 +2. 子仓 / 子模块内部状态。 + +必要时说明发布顺序: + +1. 子仓 / 子模块提交并推送。 +2. 根仓更新指针。 +3. 根仓提交并推送。 + +**检查句** + +> 当前变更发生在根仓,还是发生在子仓 / 子模块内部? + +### 9. 构建产物刷新被误认为完整发布 + +**坑点** + +本地构建成功只代表产物生成,不代表已经上线、镜像已发布、远端已更新或用户可见。 + +**规避** + +明确区分: + +- 本地构建通过。 +- 产物已写入目录。 +- 代码已提交。 +- 远端已推送。 +- CI 已通过。 +- 生产环境已部署。 +- 用户路径已验证。 + +**检查句** + +> 我现在完成的是构建、提交、发布,还是线上验证? + +### 10. 验证命令缺失却被写成通过 + +**坑点** + +本地可能缺少 PHP、Node、Python、Docker、数据库、浏览器、凭据或网络。未执行的检查不能写成通过。 + +**规避** + +验证结论分三类: + +- 通过:命令执行成功。 +- 失败:命令执行但返回错误。 +- 未执行:缺少运行时、凭据、服务或外部依赖。 + +**推荐写法** + +```text +npm run build:通过。 +phpunit:未执行,当前环境没有 php 命令。 +线上登录验证:未执行,缺少管理员登录态。 +``` + +### 11. 长输出污染判断 + +**坑点** + +全仓搜索、构建输出、生成文件和依赖目录可能产生大量无关内容。直接跟着长输出改,容易误动生成文件或依赖目录。 + +**规避** + +- 优先限定目录。 +- 排除 `node_modules`、`vendor`、`dist`、`build`、大型 bundle。 +- 对输出只提取与任务相关的证据。 + +**推荐** + +```powershell +rg -n --glob "*.php" --glob "!vendor/**" "keyword" "app" +rg -n --glob "*.ts" --glob "!node_modules/**" "keyword" "src" +``` + +### 12. Windows PowerShell 命令写法不安全 + +**坑点** + +Windows 路径包含盘符、空格、中文和反斜杠。命令不加引号或跨 shell 拼接,容易造成路径逃逸或执行失败。 + +**规避** + +- 所有路径用双引号。 +- 不用 `cmd /c` 包一层。 +- 多路径操作拆成多条命令。 +- 文件修改优先使用内置编辑工具或 `apply_patch`。 +- 大段 PowerShell 逻辑写入临时脚本再执行。 + +**推荐** + +```powershell +git -C "E:\code\project" status --short +npm run build +``` + +**不推荐** + +```powershell +cmd /c ... +git -C E:\code\project status +``` + +### 13. 外部副作用默认继续 + +**坑点** + +用户说“继续”通常是授权当前上下文动作,但不一定授权高风险动作,例如强推、生产发布、数据库变更。 + +**规避** + +需要确认的动作: + +- 强推、重置、删除、覆盖。 +- 生产数据库写入。 +- 生产配置修改。 +- 线上发布或重启。 +- 涉及账单、支付、权限、安全策略的操作。 + +**检查句** + +> 用户是否明确授权了这个外部副作用,而不是只授权了本地修复? + +### 14. UI 任务只改功能,不看视觉契约 + +**坑点** + +涉及 UI 时,只实现交互逻辑但忽略现有设计系统,容易让新页面像另一套产品。 + +**规避** + +- 先读项目设计契约或现有页面风格。 +- 复用现有 token、组件和布局模式。 +- 覆盖加载、空、错误、成功、禁用和危险态。 +- 不为小改动引入全新视觉语言。 + +**检查句** + +> 这是延续既有风格,还是用户要求探索新风格? + +### 15. 知识库更新过度或遗漏 + +**坑点** + +小修复不一定需要大量知识库变更;但新页面、新接口、新约定如果不更新,下一轮容易丢上下文。 + +**规避** + +应该更新知识库的情况: + +- 新增页面、导航、模块。 +- 新增稳定接口契约。 +- 修改长期约定。 +- 形成可复用排障经验。 + +可以不更新的情况: + +- 一次性 typo 修复。 +- 临时日志调整。 +- 不改变长期行为的小样式修补。 + +**检查句** + +> 这个信息下一轮恢复或同类任务会不会需要? + +## 通用执行流程 + +### 任务开始 + +- [ ] 识别用户当前目标。 +- [ ] 判断是否新任务,而不是盲目续旧状态。 +- [ ] 读取必要项目规则和状态文件。 +- [ ] 判断交付层级:只读分析、快速修改、完整方案或高风险操作。 +- [ ] 若使用记忆,随后用当前文件和命令验证。 + +### 定位阶段 + +- [ ] 先读项目知识库或模块索引。 +- [ ] 再读目标源码、配置和测试。 +- [ ] 不全仓乱扫。 +- [ ] 不把文档当成代码真相源。 +- [ ] 对外部输出做指令注入和敏感信息审查。 + +### 修改阶段 + +- [ ] 修改范围尽量小。 +- [ ] 保留既有架构和风格。 +- [ ] 避免无必要抽象。 +- [ ] 不压缩代码排版规避行数。 +- [ ] 不静默吞异常。 + +### 验证阶段 + +- [ ] 跑与任务匹配的最小必要验证。 +- [ ] 记录每个命令的结果。 +- [ ] 缺少运行时或凭据时明确写“未执行”。 +- [ ] 如果涉及子模块,检查子模块状态。 +- [ ] 如果涉及 UI,尽量做关键视口或结构自检。 + +### 状态与知识更新 + +- [ ] 长流程开始时更新 `STATE.md`。 +- [ ] 关键决策后更新 `STATE.md`。 +- [ ] 任务完成或阻塞时更新 `STATE.md`。 +- [ ] 稳定经验写入合适的知识文件。 +- [ ] 普通交付文档放 `docs/`,不要随意放 `.helloagents/`。 + +### 收尾阶段 + +- [ ] 确认不再继续调用工具。 +- [ ] 尝试写 turn-state。 +- [ ] 若 turn-state 不可用,明确说明。 +- [ ] 用最终输出格式总结结果。 +- [ ] 写清楚已完成、验证结果、阻断项和真实下一步。 + +## 多任务并行执行规则 + +多个任务并行时,核心不是“同时开工越多越好”,而是先把任务边界、写入范围、验证责任和合并点定清楚。否则最容易出现状态错乱、文件互相覆盖、验证结果串线和收尾漏项。 + +### 1. 先建立任务矩阵 + +并行前先把所有任务写成矩阵,不要只在脑中记。 + +```text +任务 A:目标 / 写入范围 / 验证命令 / 阻塞项 / 当前状态 +任务 B:目标 / 写入范围 / 验证命令 / 阻塞项 / 当前状态 +任务 C:目标 / 写入范围 / 验证命令 / 阻塞项 / 当前状态 +``` + +每个任务至少明确: + +- 目标:要交付什么。 +- 范围:允许读写哪些目录或文件。 +- 依赖:是否依赖其他任务的结果。 +- 验证:用什么命令或证据验收。 +- 风险:是否有外部副作用或不可逆操作。 +- 状态:待办、进行中、阻塞、待合并、已完成。 + +**规则** + +如果无法为任务写出清晰矩阵,说明还不能并行。 + +### 2. 区分关键路径和旁路任务 + +主代理应优先处理关键路径,不要把马上要用的阻塞信息丢给并行任务等待。 + +```text +关键路径:下一步必须依赖它,主代理本地做。 +旁路任务:可独立推进,不阻塞主线,可并行。 +``` + +适合并行的任务: + +- 独立模块的只读分析。 +- 不同目录、不同文件的互不重叠修改。 +- 一边实现,一边做独立验证或文档整理。 +- 后端接口梳理和前端 UI 草案分开推进。 + +不适合并行的任务: + +- 多个任务同时改同一个文件。 +- 一个任务需要另一个任务的结论才能开始。 +- 高风险发布、数据库操作、权限操作。 +- 需求还没拆清楚,边界不明确。 + +### 3. 子代理只能在明确授权时使用 + +并行不等于自动开子代理。只有用户明确要求“并行”“委托”“开子代理”“多代理处理”时,才使用子代理。 + +未获得明确授权时: + +- 主代理可以按任务矩阵顺序推进。 +- 可以本地交替处理多个任务。 +- 不应擅自 spawn 子代理。 + +获得授权后: + +- 子代理任务必须具体、有限、可独立完成。 +- 子代理写入范围必须和其他任务不重叠。 +- 子代理不得代写主代理收尾状态。 +- 主代理负责最终审查、集成和统一验证。 + +### 4. 写入范围必须隔离 + +并行修改前,为每条任务分配写入范围。 + +```text +任务 A:只改 app/Services/* +任务 B:只改 admin-frontend/src/views/orders/* +任务 C:只改 docs/* +``` + +如果两个任务必须改同一个文件: + +- 不要并行写。 +- 由主代理串行合并。 +- 或先做只读分析,再由一个任务统一改。 + +**规则** + +并行写入的最低要求是“文件级不重叠”。更稳妥的是“目录级不重叠”。 + +### 5. 状态文件要记录并行泳道 + +并行任务进行中,`STATE.md` 不能只写一个“正在做什么”。应记录每条泳道。 + +推荐结构: + +```text +## 主线目标 +并行推进 A / B / C 三个任务。 + +## 正在做什么 +- A:进行中,正在修改 ... +- B:阻塞,缺少 ... +- C:待验证,已完成 ... + +## 合并点 +所有任务完成后统一运行 ... + +## 阻塞项 +- B 需要 ... +``` + +### 6. 验证结果必须按任务归属记录 + +并行任务最容易把验证结果混在一起。每个任务必须有自己的验证证据。 + +```text +任务 A +- npm run build:通过 + +任务 B +- phpunit tests/Unit/BTest.php:未执行,缺少 php + +任务 C +- markdown 文件存在性检查:通过 +``` + +统一验证只能作为最终集成验证,不能替代每个任务的局部验证。 + +### 7. 合并前做冲突和范围复核 + +并行任务完成后,先复核: + +- 是否有两个任务改了同一文件。 +- 是否有任务改出了自己的范围。 +- 是否有生成产物被误纳入。 +- 是否有配置、锁文件、子模块指针被意外改变。 +- 是否需要重新跑统一验证。 + +推荐命令: + +```powershell +git status --short +git diff --stat +git diff --check +``` + +按项目实际路径补充更精确的验证命令。 + +### 8. 收尾只做一次,由主代理统一完成 + +并行任务不各自收尾。最终收尾由主代理统一完成: + +- 汇总所有任务完成项。 +- 汇总每条任务验证结果。 +- 写清楚未执行验证和阻断项。 +- 更新 `STATE.md` 为最终状态。 +- 尝试写 turn-state。 +- 最后一条回复再使用最终输出格式。 + +### 9. 并行任务失败时的处理 + +某个并行任务失败时,不要让它污染其他任务。 + +处理顺序: + +1. 标记该任务为阻塞或失败。 +2. 记录失败命令、错误摘要和影响范围。 +3. 判断其他任务是否依赖它。 +4. 不依赖则继续推进其他任务。 +5. 依赖则暂停合并,先修复阻塞。 + +**规则** + +局部失败不等于全部失败;但最终收尾必须清楚说明哪些完成、哪些阻塞。 + +### 10. 并行执行检查清单 + +- [ ] 已列出任务矩阵。 +- [ ] 已标记关键路径和旁路任务。 +- [ ] 未经明确授权时,没有擅自使用子代理。 +- [ ] 每个并行任务都有独立写入范围。 +- [ ] 没有两个任务同时改同一文件。 +- [ ] `STATE.md` 记录了并行泳道和合并点。 +- [ ] 每个任务都有自己的验证结论。 +- [ ] 合并前已检查 `git status`、`git diff --stat`、`git diff --check`。 +- [ ] 最终收尾由主代理统一完成。 + +## 高风险动作确认清单 + +以下动作不能只凭状态文件或历史上下文直接做: + +- 强推、硬重置、删除分支。 +- 删除文件夹、递归清理、覆盖远端。 +- 数据库删除、截断、批量更新生产数据。 +- 修改生产密钥、权限、安全策略。 +- 发布镜像、打 tag、重启线上服务。 +- 产生费用的云资源操作。 +- 发送邮件、通知、Webhook 或对用户可见的批量消息。 + +确认时只问当前阻塞的唯一决策,不要把本可执行的本地步骤也拿去确认。 + +## 推荐最终汇报结构 + +```text +完成内容 +- 改了什么 +- 文件在哪里 + +验证结果 +- 命令:通过 / 失败 / 未执行及原因 + +注意事项 +- 子模块 / 发布 / 线上验证 / 外部依赖 + +下一步 +- 当前最真实的后续动作 +``` + +## 可复制的通用检查清单 + +- [ ] 当前任务目标已按用户最新消息确认。 +- [ ] 旧 `STATE.md` 未覆盖新需求。 +- [ ] 修改范围与交付层级匹配。 +- [ ] 没有未授权外部副作用。 +- [ ] 路径命令符合当前 Shell 安全规则。 +- [ ] 源码修改优先于生成产物修改。 +- [ ] 子仓 / 子模块状态已单独检查。 +- [ ] 已执行可执行的验证命令。 +- [ ] 未执行验证已说明原因。 +- [ ] `STATE.md` 已更新到下一轮可恢复。 +- [ ] turn-state 已尝试写入或已记录不可用原因。 +- [ ] 最终回复只包含真实完成项、验证项、阻断项和下一步。 + +## 反模式速查 + +- 只看旧状态文件就继续做。 +- 中间消息套最终 HelloAGENTS 格式。 +- 工具失败后不提。 +- 把本地构建说成线上已发布。 +- 把未执行测试写成通过。 +- 忽略子模块内部状态。 +- 在 `.helloagents/` 里随意放普通文档。 +- 为小修复创建过重方案包。 +- 用记忆替代当前代码证据。 +- PowerShell 路径不加引号。 +- 高风险外部操作不确认。 +- 最后一条回复只写建议,不写实际执行结果。 diff --git a/docs/helloagents-workflow-pitfalls.md b/docs/helloagents-workflow-pitfalls.md new file mode 100644 index 0000000..22e1af9 --- /dev/null +++ b/docs/helloagents-workflow-pitfalls.md @@ -0,0 +1,342 @@ +# HelloAGENTS 工作流踩坑复盘 + +## 适用范围 + +本文记录本轮在 `E:\code\php\Xboard-new` 中调用 HelloAGENTS Standby 工作流时踩到的坑,适用于后续在 Windows + PowerShell + 已激活 `.helloagents/` 项目中执行编码、验证、文档沉淀和收尾任务。 + +## 总原则 + +1. 当前用户消息优先于旧状态文件。 +2. 代码和真实命令输出优先于记忆、文档和推断。 +3. 工具失败必须记录为验证结论或阻断项,不能静默跳过。 +4. `output_format=true` 时,只在最后一条收尾消息使用 HelloAGENTS 输出格式。 +5. 涉及 `public/assets/admin` 时,必须同时看根仓和子模块状态。 + +## 坑点清单 + +### 1. `scripts/turn-state.mjs` 被协议要求,但项目中不存在 + +**表现** + +- 收尾协议要求先执行 `scripts/turn-state.mjs write`。 +- 实际执行 `node "scripts\turn-state.mjs" write kind=complete role=main` 时失败: + - `Cannot find module 'E:\code\php\Xboard-new\scripts\turn-state.mjs'` +- 仓库根目录也没有 `scripts/` 目录。 + +**根因** + +HelloAGENTS 协议假设存在统一的 turn-state 脚本,但当前项目和全局运行目录没有提供该脚本。 + +**正确处理** + +- 收尾前按协议尝试一次。 +- 若脚本不存在,明确记录失败原因。 +- 不要反复重试同一个不存在的路径。 +- 不要因为脚本失败而伪装成 turn-state 已写入。 +- 用项目状态文件 `.helloagents/sessions/master/default/STATE.md` 补充记录当前任务状态。 + +**下次规则** + +如果仓库仍没有 `scripts/turn-state.mjs`: + +```powershell +node "scripts\turn-state.mjs" write kind=complete role=main +``` + +失败后只记录一次: + +> turn-state 脚本缺失,无法写入运行态信号;已更新 `.helloagents/.../STATE.md` 作为恢复状态。 + +### 2. 旧 `STATE.md` 容易把新任务带偏 + +**表现** + +进入本轮时,状态文件仍记录上一个“用户管理高级筛选活跃状态”任务,而用户实际要求是修复节点 TUIC 和权限组问题。 + +**根因** + +`.helloagents/sessions/master/default/STATE.md` 是恢复参考,不是当前任务真相源。若直接按旧状态续作,会进入错误业务线。 + +**正确处理** + +- 先读当前用户消息。 +- 再看活跃方案包、代码、验证证据。 +- 最后才用 `STATE.md` 补上下文。 +- 确认是新任务后,立即重写 `STATE.md`,不要等到收尾。 + +**下次规则** + +判断主线优先级固定为: + +1. 当前用户最新消息。 +2. 本轮确认的范围。 +3. 当前代码与验证证据。 +4. 活跃方案包。 +5. `STATE.md`。 + +### 3. 输出格式只能用于最后一条消息 + +**表现** + +配置中 `output_format=true`,但工具执行前、执行中、阻塞说明都不能使用 HelloAGENTS 外层格式。 + +**根因** + +输出格式只允许用于本轮最终收尾消息。中间消息如果套格式,会让运行时误判任务完成或等待输入。 + +**正确处理** + +- 中间状态用自然语言或工具计划,不使用 `{图标}【HelloAGENTS】`。 +- 确认不再调用工具后,再使用最终格式。 +- 如果使用了记忆引用,记忆引用块必须放在最终回复最末尾。 + +**下次规则** + +只在满足以下条件时使用 HelloAGENTS 输出格式: + +- 不再调用工具。 +- 不再继续执行。 +- 已完成或已明确阻塞。 +- 已尝试写 turn-state,或明确记录脚本缺失。 + +### 4. `.helloagents/` 不是随便放文件的目录 + +**表现** + +用户要求“提炼成一个 md 文档”,直觉上可以放到 `.helloagents/`,但协议要求 `.helloagents/` 内文件创建和更新必须遵循模板。 + +**根因** + +`.helloagents/` 是运行态和知识库目录,部分文件有固定模板和生命周期;随意新增文档可能破坏流程约定。 + +**正确处理** + +- 临时复盘、交付文档优先放到 `docs/`。 +- 只有状态、方案、知识库、归档等 HelloAGENTS 定义文件才放入 `.helloagents/`。 +- 更新 `.helloagents/CHANGELOG.md`、`modules/*.md` 时保持既有格式。 + +**下次规则** + +用户要普通 Markdown 成品时,默认使用: + +```text +docs/.md +``` + +除非用户明确要求放进 `.helloagents/`。 + +### 5. 容易把快速修复膨胀成完整方案包 + +**表现** + +节点权限组和 TUIC 模板是明确小范围修复,容易因为项目已激活而误判为必须创建完整 `plans/{feature}/`。 + +**根因** + +HelloAGENTS 有 T0-T3 交付分层,但不是所有激活项目任务都必须走完整方案包。明确、低风险、小范围、可验证的变更可按 T1 快速执行。 + +**正确处理** + +- 先判断任务复杂度。 +- 明确 bugfix 可以直接修,不用额外确认。 +- 无独立方案包时,在 `CHANGELOG.md` 标记“快速修改(无方案包)”。 + +**下次规则** + +符合以下条件时走快速修改: + +- 用户描述了具体故障或具体页面。 +- 改动范围可限定在少量文件。 +- 不涉及生产高风险操作。 +- 可用本地构建或静态检查验证。 + +### 6. 记忆可辅助定位,但不能替代当前证据 + +**表现** + +记忆中已有 Xboard-new、`admin-frontend`、`public/assets/admin` 子模块等经验,能快速提醒验证路径和历史坑。 + +**风险** + +记忆可能过期。直接把记忆当作当前事实,会漏掉分支、远端、目录结构变化。 + +**正确处理** + +- 先用记忆找关键词和历史坑。 +- 再用当前命令验证: + - `git status` + - `npm run build` + - 子模块状态 + - 目标文件实际内容 +- 最终报告区分“已验证”和“记忆提示”。 + +**下次规则** + +记忆只能回答“应该重点看哪里”,不能回答“当前一定是什么状态”。 + +### 7. `public/assets/admin` 是子模块,根仓状态不等于真实前端产物状态 + +**表现** + +执行 `npm run build` 后,根仓只显示: + +```text +m public/assets/admin +``` + +这不是“没改到”,而是子模块内部产物发生变化。 + +**额外坑** + +进入子模块执行 `git status` 时出现: + +```text +fatal: detected dubious ownership in repository at 'E:/code/php/Xboard-new/public/assets/admin' +``` + +**正确处理** + +使用单次 scoped safe directory,不要默认改全局配置: + +```powershell +git -c safe.directory="E:/code/php/Xboard-new/public/assets/admin" -C "E:\code\php\Xboard-new\public\assets\admin" status --short --branch +``` + +**下次规则** + +涉及管理端构建产物时必须给两层证据: + +1. 根仓:源码文件变更 + `m public/assets/admin`。 +2. 子模块:产物文件状态或关键词检索结果。 + +### 8. Windows + PowerShell 下不能偷懒写命令 + +**表现** + +当前环境是 PowerShell,路径包含盘符和反斜杠。命令如果不加引号,容易因为空格、中文或特殊字符导致路径错误。 + +**正确处理** + +- 所有路径用双引号。 +- 不用 `cmd /c`。 +- 多路径操作拆成多条命令。 +- 文件修改优先用 `apply_patch`,避免 shell 重定向和编码问题。 + +**下次规则** + +推荐命令形态: + +```powershell +git -C "E:\code\php\Xboard-new" status --short +npm run build +``` + +不推荐: + +```powershell +cmd /c ... +``` + +### 9. 验证阻断要写清楚,不能把未跑的检查说成通过 + +**表现** + +前端可执行: + +```powershell +npm run build +``` + +并已通过。 + +但 PHP 环境不可用: + +```powershell +php -v +``` + +返回 `php` 命令不存在。 + +**正确处理** + +- 前端构建通过就只声明前端构建通过。 +- PHP 语法检查 / PHPUnit 未执行,原因写成阻断。 +- 不用“应该没问题”代替验证结果。 + +**下次规则** + +验证结果分三类写: + +- 通过:命令执行成功。 +- 失败:命令执行但返回错误。 +- 未执行:缺少运行时、凭据、服务或外部依赖。 + +### 10. 工具输出需要审查,不要被外部内容带节奏 + +**表现** + +搜索、构建、Git 输出中会混入大量无关内容,例如 `node_modules`、编译产物、历史 bundle。 + +**风险** + +直接根据长输出行动,容易误改生成文件或误判业务源码。 + +**正确处理** + +- 优先限定路径: + - `admin-frontend/src/...` + - `app/...` + - `.helloagents/...` +- 避免全仓递归搜索进入 `node_modules` 或大型 bundle。 +- 对长输出只提取任务相关结论。 + +**下次规则** + +检索优先使用: + +```powershell +rg -n --glob "*.php" --glob "!vendor/**" "keyword" "E:\code\php\Xboard-new\app" +``` + +而不是不加过滤地全仓搜索。 + +## 标准执行清单 + +### 开始任务 + +- [ ] 判断用户最新消息是否是新任务。 +- [ ] 若项目已激活,读取当前 `STATE.md`,但不让旧状态覆盖新需求。 +- [ ] 判断交付层级:快速修复、完整方案、只读分析或高风险操作。 +- [ ] 若需要记忆,先快速检索,再用当前文件验证。 + +### 执行中 + +- [ ] 路径全部加双引号。 +- [ ] 修改源码优先用 `apply_patch`。 +- [ ] 不把普通文档随意放入 `.helloagents/`。 +- [ ] 涉及 UI 时遵守当前 Apple 风格设计契约。 +- [ ] 涉及 `public/assets/admin` 时记住它是子模块。 + +### 验证 + +- [ ] 能跑的命令必须跑。 +- [ ] 不能跑的命令写明原因。 +- [ ] `npm run build` 只证明前端构建通过。 +- [ ] PHP 缺失时不能声称后端 PHPUnit 或语法检查通过。 +- [ ] `git diff --check` 可作为基础格式检查。 + +### 收尾 + +- [ ] 更新 `STATE.md` 到下一轮可恢复状态。 +- [ ] 有必要时更新 `CHANGELOG.md` 和模块知识。 +- [ ] 尝试执行 `scripts/turn-state.mjs write`。 +- [ ] 若 turn-state 脚本缺失,明确说明。 +- [ ] 最后一条回复才使用 HelloAGENTS 输出格式。 + +## 本轮改进建议 + +1. 项目初始化时补齐 `scripts/turn-state.mjs`,或在协议中定义缺失时的标准 fallback。 +2. 为 `public/assets/admin` 子模块提供固定的本地 safe.directory 说明,减少每轮踩同一个 Git 安全检查。 +3. 在 `.helloagents/verify.yaml` 中区分前端、后端、子模块验证命令,避免只剩单一 `npm run build`。 +4. 给“快速修改(无方案包)”建立轻量记录模板,避免每次临时判断写法不一致。 +5. 将 Windows PowerShell 安全命令模板沉淀为项目级约定,减少路径和编码风险。