diff --git a/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java b/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java
index 1383ede2..58468044 100644
--- a/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java
+++ b/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java
@@ -1386,14 +1386,16 @@ public class WebChatController {
}
/**
- * 重新生成最后一条助手回复。
+ * Regenerate the last assistant reply.
*
- * 语义:找到会话最后一条 {@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.
*
- * 没有任何 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)
diff --git a/mateclaw-server/src/main/java/vip/mate/team/service/TeamTaskService.java b/mateclaw-server/src/main/java/vip/mate/team/service/TeamTaskService.java
index d82f35ba..5b64e7a8 100644
--- a/mateclaw-server/src/main/java/vip/mate/team/service/TeamTaskService.java
+++ b/mateclaw-server/src/main/java/vip/mate/team/service/TeamTaskService.java
@@ -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 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);
}
diff --git a/mateclaw-server/src/main/java/vip/mate/team/tool/TeamTasksTool.java b/mateclaw-server/src/main/java/vip/mate/team/tool/TeamTasksTool.java
index 2a13cd1a..90e1fb29 100644
--- a/mateclaw-server/src/main/java/vip/mate/team/tool/TeamTasksTool.java
+++ b/mateclaw-server/src/main/java/vip/mate/team/tool/TeamTasksTool.java
@@ -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,
diff --git a/mateclaw-server/src/main/resources/docs/en/channels.md b/mateclaw-server/src/main/resources/docs/en/channels.md
index 9a47b115..a6d77e31 100644
--- a/mateclaw-server/src/main/resources/docs/en/channels.md
+++ b/mateclaw-server/src/main/resources/docs/en/channels.md
@@ -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 ` or `/model :` 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.
---
diff --git a/mateclaw-server/src/main/resources/docs/en/models.md b/mateclaw-server/src/main/resources/docs/en/models.md
index 6aac7305..f815614b 100644
--- a/mateclaw-server/src/main/resources/docs/en/models.md
+++ b/mateclaw-server/src/main/resources/docs/en/models.md
@@ -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)
diff --git a/mateclaw-server/src/main/resources/docs/en/teams.md b/mateclaw-server/src/main/resources/docs/en/teams.md
index c079f647..0934154b 100644
--- a/mateclaw-server/src/main/resources/docs/en/teams.md
+++ b/mateclaw-server/src/main/resources/docs/en/teams.md
@@ -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.
---
diff --git a/mateclaw-server/src/main/resources/docs/zh/channels.md b/mateclaw-server/src/main/resources/docs/zh/channels.md
index c3a7dd04..86b6edbe 100644
--- a/mateclaw-server/src/main/resources/docs/zh/channels.md
+++ b/mateclaw-server/src/main/resources/docs/zh/channels.md
@@ -664,10 +664,10 @@ IM 渠道(企业微信、微信、钉钉)都支持语音输入。语音识
| `/clear` | 清空当前会话上下文(保留会话本身;1.8 时代的 clear 命令并入本框架) |
| `/status` | 查看当前会话状态——绑定的员工、模型、是否有任务在跑 |
| `/stop` | 停止正在执行的任务——在入队口拦截,可抢在长任务中途生效 |
-| `/model` | 列出可用模型 / 按名切换**本会话**的模型,切换只影响当前会话 |
+| `/model` | 不带参数列出可用模型(标出当前钉选);`/model <名称>` 或 `/model <提供商>:<名称>` 切换**本会话**模型,下一条消息生效;`/model reset` 恢复默认。名称模糊时给出候选建议 |
| `/help` | 列出全部可用命令与说明 |
-每条命令都带中英文别名,大小写不敏感、整串精确匹配——普通消息里包含 `/stop` 字样不会误触发。命令确认消息走渠道的正常渲染发送链路,所以已经贴出的"思考中"占位气泡会被正确消掉,不会留下一个永远转圈的气泡。
+每条命令都带中英文别名(如 `清空` / `新会话` / `状态`),大小写不敏感。匹配规则有两层:**裸别名只做整条消息精确匹配**("帮助我写周报"是正常提问,不会触发 `/help`);**斜杠形式按首词匹配、参数透传**(所以 `/model qwen-max` 能带参数)。正文里夹着 `/stop` 字样的普通消息不会误触发。命令确认消息走渠道的正常渲染发送链路,所以已经贴出的"思考中"占位气泡会被正确消掉,不会留下一个永远转圈的气泡。
---
diff --git a/mateclaw-server/src/main/resources/docs/zh/models.md b/mateclaw-server/src/main/resources/docs/zh/models.md
index 808c61a7..847245fc 100644
--- a/mateclaw-server/src/main/resources/docs/zh/models.md
+++ b/mateclaw-server/src/main/resources/docs/zh/models.md
@@ -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)
diff --git a/mateclaw-server/src/main/resources/docs/zh/teams.md b/mateclaw-server/src/main/resources/docs/zh/teams.md
index 502febd8..65a302cd 100644
--- a/mateclaw-server/src/main/resources/docs/zh/teams.md
+++ b/mateclaw-server/src/main/resources/docs/zh/teams.md
@@ -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` 叫帮手。
---
diff --git a/mateclaw-server/src/test/java/vip/mate/team/service/TeamTaskServiceTest.java b/mateclaw-server/src/test/java/vip/mate/team/service/TeamTaskServiceTest.java
index 9bb485cb..97caa222 100644
--- a/mateclaw-server/src/test/java/vip/mate/team/service/TeamTaskServiceTest.java
+++ b/mateclaw-server/src/test/java/vip/mate/team/service/TeamTaskServiceTest.java
@@ -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() {
diff --git a/mateclaw-ui/src/components/chat/preview/previewKind.ts b/mateclaw-ui/src/components/chat/preview/previewKind.ts
index 62b48adb..750d3c67 100644
--- a/mateclaw-ui/src/components/chat/preview/previewKind.ts
+++ b/mateclaw-ui/src/components/chat/preview/previewKind.ts
@@ -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).