mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(team): gate pending-task auto-claim to the assignee; expose attach in tool schema
This commit is contained in:
parent
daa2c8b9ac
commit
3643aed756
@ -1386,14 +1386,16 @@ public class WebChatController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成最后一条助手回复。
|
||||
* Regenerate the last assistant reply.
|
||||
* <p>
|
||||
* 语义:找到会话最后一条 {@code role=user} 消息 → stop 当前流(如有)→ 删除最后一条
|
||||
* {@code role=assistant} 消息 → 用 last user message 重新启动 agent turn。
|
||||
* 实际启动复用 {@link #chatStream},它会重新 saveMessage user(新消息 id,内容相同)。
|
||||
* 这样不重复 100 行 SSE 代码,代价是用户消息多一条(语义上等同"重发")。
|
||||
* Semantics: stop any in-flight stream, rewind the conversation to the last
|
||||
* {@code role=user} message (removing the trailing assistant reply), then
|
||||
* re-run the agent turn from that message. The restart reuses
|
||||
* {@link #chatStream} with {@code internalSkipUserPersist} set, so the
|
||||
* existing user row is used as the seed and no duplicate user message is
|
||||
* inserted.
|
||||
* <p>
|
||||
* 没有任何 user 消息时返回 400(无内容可重新生成)。
|
||||
* Returns an error when the conversation has no user message to regenerate from.
|
||||
*/
|
||||
@Operation(summary = "重新生成最后一条助手回复")
|
||||
@PostMapping(value = "/sessions/regenerate", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
|
||||
@ -168,8 +168,10 @@ public class TeamTaskService {
|
||||
|
||||
/**
|
||||
* Complete a task with a result summary. A pending task is auto-claimed
|
||||
* first (single-call convenience; safe because the claim is atomic). When
|
||||
* the task requires approval it parks in in_review instead of completed.
|
||||
* first (single-call convenience; safe because the claim is atomic), but
|
||||
* only by its assignee — otherwise any team member could complete another
|
||||
* member's not-yet-dispatched task. When the task requires approval it
|
||||
* parks in in_review instead of completed.
|
||||
*
|
||||
* @return ids of dependent tasks released to pending by this completion
|
||||
*/
|
||||
@ -177,6 +179,10 @@ public class TeamTaskService {
|
||||
public List<Long> completeTask(Long taskId, Long agentId, String result) {
|
||||
TeamTaskEntity task = requireTask(taskId);
|
||||
if (TeamTaskStatus.PENDING.equals(task.getStatus()) && agentId != null) {
|
||||
if (task.getAssigneeAgentId() != null && !agentId.equals(task.getAssigneeAgentId())) {
|
||||
throw new IllegalStateException("task #" + task.getTaskNumber()
|
||||
+ " is assigned to another agent; only the assignee can claim and complete it");
|
||||
}
|
||||
claimTask(taskId, agentId);
|
||||
task = requireTask(taskId);
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ public class TeamTasksTool {
|
||||
+ "'retry' a failed/stale task back to pending (lead only; taskId). "
|
||||
+ "Only usable when you belong to an agent team.")
|
||||
public String team_tasks(
|
||||
@ToolParam(description = "One of: list, get, create, complete, progress, comment, cancel, retry")
|
||||
@ToolParam(description = "One of: list, get, create, complete, progress, comment, attach, cancel, retry")
|
||||
String action,
|
||||
@ToolParam(description = "Task id (string form is fine) — required by every action except list/create", required = false)
|
||||
String taskId,
|
||||
|
||||
@ -664,10 +664,10 @@ In any IM channel, a message that is entirely a `/`-prefixed command is intercep
|
||||
| `/clear` | Clear the current conversation's context (the conversation itself survives; the 1.8-era clear command folds into this framework) |
|
||||
| `/status` | Show the conversation's state — bound employee, model, whether a task is running |
|
||||
| `/stop` | Stop the running task — intercepted at the enqueue gate, so it preempts a long task mid-flight |
|
||||
| `/model` | List available models / switch **this conversation's** model by name; the switch affects only the current conversation |
|
||||
| `/model` | With no argument, list available models (current pin marked); `/model <name>` or `/model <provider>:<name>` switches **this conversation's** model, effective from the next message; `/model reset` restores the default. Fuzzy names get suggestion lists |
|
||||
| `/help` | List all commands with descriptions |
|
||||
|
||||
Every command carries Chinese and English aliases, is case-insensitive, and matches the whole message exactly — a normal message that merely contains `/stop` never misfires. Command confirmations go through the channel's normal render-and-send path, so an already-posted "thinking…" placeholder bubble is properly consumed instead of spinning forever.
|
||||
Every command carries Chinese and English aliases (e.g. `清空` / `新会话` / `状态`) and is case-insensitive. Matching is two-layered: **bare aliases match only as the entire message** ("help me write a report" is a normal prompt, not `/help`); **the slash form matches on the first token with arguments passed through** (which is how `/model qwen-max` carries its argument). A normal message that merely contains `/stop` mid-sentence never misfires. Command confirmations go through the channel's normal render-and-send path, so an already-posted "thinking…" placeholder bubble is properly consumed instead of spinning forever.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -412,7 +412,7 @@ Failover decides *who to switch to*; 2.0.0 also makes *how each error recovers*
|
||||
- **"Server overloaded" and "my key is rate-limited" are treated differently.** A new OVERLOADED class: 503/529-style **server overload** means everyone is queuing — switching providers just burns the whole chain for nothing (and single-key users have nowhere to switch) — so the right move is **back off on the same provider**; a 429 on **your own key** is what deserves a fast rotation. These used to be conflated with opposite policies.
|
||||
- **When the provider says when it recovers, we believe it.** `Retry-After` / ratelimit-reset response headers used to go only to logs; they now **feed directly into backoff duration and health cooldown** — no more blind backoff against a known rate-limit window.
|
||||
- **Eviction is a TTL cooldown, not a death sentence.** Providers hard-evicted for auth failures or billing now get TTL-based readmission (swap in a new key or top up the account and the system heals itself, no restart required); a provider-stated recovery time overrides the default.
|
||||
- **Jittered backoff prevents retry storms.** Concurrent conversations hitting the same rate-limited provider back off with decorrelated jitter — no more lockstep mass retries that keep re-triggering the limit.
|
||||
- **Randomized jitter prevents retry storms.** Concurrent conversations hitting the same rate-limited provider back off with randomized jitter (±30% on the overload backoff tiers, exponential backoff plus a random component on the generic path) — no more lockstep mass retries that keep re-triggering the limit.
|
||||
|
||||
### Preferred provider drives the primary model (1.5.0)
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ head:
|
||||
|
||||
> **Before: one employee with sub-tasks. Now: a team around a shared task board.**
|
||||
|
||||
Sub-agent delegation (`delegate_agent`) solves "one person temporarily calls a helper": synchronous, one-to-one, black-box. But real complex delivery looks like a project: **break down tasks, declare dependencies, run in parallel, gate on approvals, archive deliverables, and see who is doing what at any time**.
|
||||
Sub-agent delegation (`delegateToAgent`) solves "one person temporarily calls a helper": synchronous, one-to-one, black-box. But real complex delivery looks like a project: **break down tasks, declare dependencies, run in parallel, gate on approvals, archive deliverables, and see who is doing what at any time**.
|
||||
|
||||
Agent Teams bring that project machinery into MateClaw: you create a **team**, assign one **lead** employee and several **members**; tell the lead a goal, it breaks the goal into tasks on a **shared task board**; the dispatch engine hands tasks to members and runs them **in parallel**; settled results are announced back to the lead, which reviews, re-dispatches, and drives the whole thing to done. You watch it all from the Teams page — or drop tasks onto the board yourself.
|
||||
|
||||
@ -98,6 +98,8 @@ Leads aren't restricted by agent type. A **ReAct lead** creates tasks one by one
|
||||
- after hand-off the plan parks (`delegated`) and the lead's turn ends normally; the dispatch/announce loop takes over;
|
||||
- once all tasks settle, the wake-up passes a **parked-plan resume gate** that deterministically routes to the plan summary node, rebuilding context from task results and deliverables — the same "park in DB, resume in a fresh turn" shape the tool-approval flow already uses, with no checkpoint machinery.
|
||||
|
||||
The hand-off is **all-or-nothing**: the plan goes to the board only when every step resolves to a team member; if any step can't be assigned, the whole plan falls back to the original serial delegation pipeline, behaving exactly as before.
|
||||
|
||||
In short: **a lead that can plan turns its planning into team orchestration.**
|
||||
|
||||
---
|
||||
@ -137,9 +139,9 @@ Data lives in five tables: `mate_agent_team`, `mate_agent_team_member`, `mate_te
|
||||
|
||||
---
|
||||
|
||||
## Teams vs. sub-agent delegation (`delegate_agent`)
|
||||
## Teams vs. sub-agent delegation (`delegateToAgent`)
|
||||
|
||||
| | `delegate_agent` | Team task board |
|
||||
| | `delegateToAgent` | Team task board |
|
||||
|---|---|---|
|
||||
| Shape | One-off helper call | Standing team + shared board |
|
||||
| Parallelism | Single async call | Member-level parallelism + dependency orchestration |
|
||||
@ -147,7 +149,7 @@ Data lives in five tables: `mate_agent_team`, `mate_agent_team_member`, `mate_te
|
||||
| Interrupt/recover | Tied to parent turn | Leases, cancel-interrupt, retry |
|
||||
| Fits | Outsourcing one sub-problem | Multi-role, multi-step project delivery |
|
||||
|
||||
They coexist: a team member can still call `delegate_agent` inside its own task.
|
||||
They coexist: a team member can still call `delegateToAgent` inside its own task.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -664,10 +664,10 @@ IM 渠道(企业微信、微信、钉钉)都支持语音输入。语音识
|
||||
| `/clear` | 清空当前会话上下文(保留会话本身;1.8 时代的 clear 命令并入本框架) |
|
||||
| `/status` | 查看当前会话状态——绑定的员工、模型、是否有任务在跑 |
|
||||
| `/stop` | 停止正在执行的任务——在入队口拦截,可抢在长任务中途生效 |
|
||||
| `/model` | 列出可用模型 / 按名切换**本会话**的模型,切换只影响当前会话 |
|
||||
| `/model` | 不带参数列出可用模型(标出当前钉选);`/model <名称>` 或 `/model <提供商>:<名称>` 切换**本会话**模型,下一条消息生效;`/model reset` 恢复默认。名称模糊时给出候选建议 |
|
||||
| `/help` | 列出全部可用命令与说明 |
|
||||
|
||||
每条命令都带中英文别名,大小写不敏感、整串精确匹配——普通消息里包含 `/stop` 字样不会误触发。命令确认消息走渠道的正常渲染发送链路,所以已经贴出的"思考中"占位气泡会被正确消掉,不会留下一个永远转圈的气泡。
|
||||
每条命令都带中英文别名(如 `清空` / `新会话` / `状态`),大小写不敏感。匹配规则有两层:**裸别名只做整条消息精确匹配**("帮助我写周报"是正常提问,不会触发 `/help`);**斜杠形式按首词匹配、参数透传**(所以 `/model qwen-max` 能带参数)。正文里夹着 `/stop` 字样的普通消息不会误触发。命令确认消息走渠道的正常渲染发送链路,所以已经贴出的"思考中"占位气泡会被正确消掉,不会留下一个永远转圈的气泡。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -413,7 +413,7 @@ failover 决定"切给谁",2.0.0 把"什么错该怎么恢复"也做成了**
|
||||
- **"服务端过载"与"自己被限流"分开治**。新增 OVERLOADED 分类:503/529 这类**服务端过载**是全网都在排队,切 provider 只会把整条链白白烧一遍(单 key 用户更是无处可切)——正确动作是**同 provider 退避等待**;而 429 打在**自己 key** 上的限流才值得快速切换。以前这两种被混在一起、策略相反。
|
||||
- **Provider 说几点恢复,就几点恢复**。响应头里的 `Retry-After` / ratelimit-reset 以前只进日志,现在**直接回馈到退避时长与健康冷却**——不再对着限流窗口盲退避浪费时间。
|
||||
- **摘除不是判死,是带 TTL 的冷却**。认证失败、欠费被硬摘除的 provider 现在按 TTL 自动回收重试(例如换绑了新 key、账户充了值,系统自己恢复,不再需要人工重启);provider 明确给出恢复时刻时以其为准。
|
||||
- **抖动退避防重试风暴**。并发会话撞上同一个限流 provider 时,退避加去相关抖动——不会所有会话以同一节奏集体重试、持续触发限流。
|
||||
- **随机抖动防重试风暴**。并发会话撞上同一个限流 provider 时,退避带随机抖动(过载按档位 ±30%、通用路径指数退避外加随机分量)——不会所有会话以同一节奏集体重试、持续触发限流。
|
||||
|
||||
### 偏好提供商决定主模型(1.5.0)
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ head:
|
||||
|
||||
> **以前是"一个员工带子任务"。现在是"一个团队围着一块任务板"。**
|
||||
|
||||
子员工委派(`delegate_agent`)解决的是"一个人临时叫帮手":同步等结果、一对一、过程黑盒。但真实的复杂交付不长这样——它长得像一个项目:**拆任务、标依赖、并行推进、卡点审批、交付物归档、随时能看谁在干什么**。
|
||||
子员工委派(`delegateToAgent`)解决的是"一个人临时叫帮手":同步等结果、一对一、过程黑盒。但真实的复杂交付不长这样——它长得像一个项目:**拆任务、标依赖、并行推进、卡点审批、交付物归档、随时能看谁在干什么**。
|
||||
|
||||
团队协作把这套项目机制搬进 MateClaw:你建一个**团队**,指定一个 **Lead** 员工、若干**成员**员工;对 Lead 说一句目标,它把目标拆成任务落到**共享任务板**上;派发引擎把任务自动分给成员**并行执行**;成员完成后结果自动通报回 Lead,由它汇总、补派、直到整件事干完。你全程在 Teams 页旁观——或者直接往板上投任务。
|
||||
|
||||
@ -98,6 +98,8 @@ Lead 不限定 Agent 类型。**ReAct 型 Lead** 用 `team_tasks` 逐条建任
|
||||
- 移交后计划停靠(`delegated`),Lead 回合正常结束,等待由派发/通报闭环接管;
|
||||
- 全部任务落定后,通报唤醒经**停靠计划恢复门**确定性地路由到计划汇总节点,从任务结果与交付物重建上下文、产出总结——与工具审批"落库停靠、新一轮续跑"同构,不引入检查点机制。
|
||||
|
||||
移交是**全有或全无**:只有当计划的每一步都能落到某个团队成员头上时才整体上板;有任何一步指不到成员,整个计划回落到原有的串行委派管线,行为与从前完全一致。
|
||||
|
||||
一句话:**会规划的 Lead,规划能力直接变成团队编排能力。**
|
||||
|
||||
---
|
||||
@ -137,9 +139,9 @@ Lead 不限定 Agent 类型。**ReAct 型 Lead** 用 `team_tasks` 逐条建任
|
||||
|
||||
---
|
||||
|
||||
## 与子员工委派(delegate_agent)怎么选
|
||||
## 与子员工委派(delegateToAgent)怎么选
|
||||
|
||||
| | `delegate_agent` | 团队任务板 |
|
||||
| | `delegateToAgent` | 团队任务板 |
|
||||
|---|---|---|
|
||||
| 形态 | 一对一临时叫帮手 | 常设团队 + 共享看板 |
|
||||
| 并行 | 单点异步 | 成员级并行 + 依赖编排 |
|
||||
@ -147,7 +149,7 @@ Lead 不限定 Agent 类型。**ReAct 型 Lead** 用 `team_tasks` 逐条建任
|
||||
| 中断/恢复 | 随父会话 | 租约、取消中断、重试 |
|
||||
| 适合 | 单个子问题外包 | 多角色多步骤的项目型交付 |
|
||||
|
||||
两者共存:团队成员在自己的任务里照样可以再 `delegate_agent` 叫帮手。
|
||||
两者共存:团队成员在自己的任务里照样可以再 `delegateToAgent` 叫帮手。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -166,6 +166,38 @@ class TeamTaskServiceTest {
|
||||
verify(taskMapper, never()).update(isNull(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a non-assignee cannot auto-claim and complete a pending task")
|
||||
void completePendingByNonAssigneeRejected() {
|
||||
TeamTaskEntity t = task(5L, TeamTaskStatus.PENDING);
|
||||
t.setAssigneeAgentId(MEMBER_ID);
|
||||
when(taskMapper.selectById(5L)).thenReturn(t);
|
||||
|
||||
IllegalStateException e = assertThrows(IllegalStateException.class,
|
||||
() -> service.completeTask(5L, 3L, "hijack"));
|
||||
assertTrue(e.getMessage().contains("only the assignee"));
|
||||
// Neither the claim nor the completion update may run.
|
||||
verify(taskMapper, never()).update(isNull(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("the assignee auto-claims a pending task when completing it directly")
|
||||
void completePendingByAssigneeAutoClaims() {
|
||||
TeamTaskEntity pending = task(5L, TeamTaskStatus.PENDING);
|
||||
pending.setAssigneeAgentId(MEMBER_ID);
|
||||
TeamTaskEntity claimed = task(5L, TeamTaskStatus.IN_PROGRESS);
|
||||
claimed.setAssigneeAgentId(MEMBER_ID);
|
||||
claimed.setOwnerAgentId(MEMBER_ID);
|
||||
// First read sees pending, the re-read after the atomic claim sees in_progress.
|
||||
when(taskMapper.selectById(5L)).thenReturn(pending).thenReturn(claimed);
|
||||
when(taskMapper.update(isNull(), any())).thenReturn(1);
|
||||
when(taskMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
assertTrue(service.completeTask(5L, MEMBER_ID, "done").isEmpty());
|
||||
// Claim update plus completion update.
|
||||
verify(taskMapper, times(2)).update(isNull(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("completing a terminal task fails with a state error")
|
||||
void completeTerminalRejected() {
|
||||
|
||||
@ -7,7 +7,8 @@ import type { ChatAttachment } from '@/types'
|
||||
* - 'docx' — rendered client-side with docx-preview
|
||||
* - 'sheet' — xlsx/csv parsed client-side with exceljs
|
||||
* - 'text' — markdown / code / plain text, reuses the chat markdown renderer
|
||||
* - 'html' — rendered inside a fully sandboxed iframe (no scripts, no origin)
|
||||
* - 'html' — rendered inside a sandboxed iframe (scripts allowed, opaque
|
||||
* origin — no same-origin access)
|
||||
* - 'office' — needs server-side conversion to PDF (soffice); the frontend
|
||||
* requests `{url}/preview` and renders the result as 'pdf'.
|
||||
* Falls back to download when the server has no converter (501).
|
||||
|
||||
Loading…
Reference in New Issue
Block a user