Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ AgentHub 让你像在 IM 群聊里协作一样,把真人好友、Builder、Rev
- **IM 形态协作** — 单聊、群聊、@Agent,在同一条任务流里完成
- **多 Runtime 调度** — Claude Code、Codex、OpenCode 通过统一 Adapter 接入
- **Diff / Preview / Approval** — 代码变更内联展示,审批流可控
- **三端原生** — Tauri Desktop + Web + Expo React Native Mobile(Desktop/Web 主线Mobile 装配中
- **三端原生** — Tauri Desktop + Web + Expo React Native Mobile(Desktop/Web 主线Mobile 是装配中的 fixture/边界验证 lane,**非 release candidate** —— 发布链路的 `build-mobile` 受 `RELEASE_MOBILE_ENABLED` 门控、默认关闭
- **Hub-Edge 分布式** — 本地执行不依赖 Hub;Hub 提供多端同步、远程查看和审计

## 技术栈
Expand Down
2 changes: 1 addition & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ AgentHub lets you collaborate with human teammates and AI agents in the same gro
- **IM-native collaboration** — DMs, group chats, @agent mentions — all in one task stream
- **Multi-runtime dispatch** — Claude Code, Codex, OpenCode through a unified adapter interface
- **Diff / Preview / Approval** — inline code changes, review workflows
- **Three native clients** — Tauri Desktop + Web + Expo React Native Mobile (Desktop/Web are the mainline; Mobile in assembly)
- **Three native clients** — Tauri Desktop + Web + Expo React Native Mobile (Desktop/Web are the mainline; Mobile is an in-assembly fixture/boundary-validation lane, **not a release candidate** — the release pipeline's `build-mobile` job is gated behind `RELEASE_MOBILE_ENABLED` and skipped by default)
- **Hub-Edge distributed** — local execution does not require the Hub; the Hub adds multi-device sync, remote viewing, and audit

## Tech Stack
Expand Down
8 changes: 6 additions & 2 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ api/
| 模块 | 负责内容 | 主要归属 |
|---|---|---|
| IM / Project | Project、Conversation、Thread、Message、Item、Memory | Edge / Hub |
| Execution / Runtime | AgentRun、Approval、Artifact、Preview、Workspace、Agent Runtime adapter | Edge |
| Execution / Runtime | AgentRun、Approval、Artifact、Preview、Agent Runtime adapter | Edge |
| Profile / Configuration | Agent Profile、模型映射、Skill、MCP、cc-switch provider binding、审批策略 | Hub / Edge |
| Target / Relay | Local Edge、Remote Edge、Cloud Edge、Hub Relay command、设备状态 | Hub / Edge |
| Hub / Sync | Auth、User、Contact、Group、Device、Sync、Cloud | Hub |
| Hub / Sync | Auth、User、Contact、Group、Device、Sync、Cloud、Workspace(元数据与列举,以 `/web/projects*` 暴露;`owner: Runner` 已退役,见 `conventions.md` §OpenAPI Metadata) | Hub |

本地执行链路 `Desktop -> Local Edge -> Agent Runtime adapter -> Agent CLI` 不依赖 Hub。云端 IM、多端同步、远程查看/审批和 Hub relay 才需要 Hub session。

Expand All @@ -68,6 +68,10 @@ api/
| P3 | Hub relay、Cloud Edge、远程执行 |
| P4 | 完整联系人、群聊、团队空间、Skill/MCP/Profile 生态 |

**适用范围**:`x-agenthub-phase` 只标注 `/v1/**` 设计面。reality-face(`/client`、`/web`、`/edge`)由 `x-agenthub-status` 治理、**不带 phase**,`/health`、`/api`、`/cloud` 属 ops/非设计面同样不带 ⇒ 「284 个 operation 里 166 个没有 phase」不是覆盖率缺陷,而是这条适用范围(实测缺失分布:`/web 91`、`/client 64`、`/edge 5`、`/v1 3`、`/health`·`/api`·`/cloud` 各 1)。ADR-030 / #2258。

**已知例外 3 个(待产品定值,本轮不代拍)**:`GET /v1/metrics`、`GET /v1/agent-instances`、`POST /v1/permissions/decide` 属 `/v1/**` 却没有 phase(三者都是 `status: implemented`、原 `owner: Edge`,且都已带其他元数据)。`docs/architecture/` 全文没有给它们定过 P0~P4,同族兄弟端点的 phase 也是 P0/P1/P2/P4 混杂、无法照抄 ⇒ 本轮**只声明规则、不代产品填值**:填一个没有出处的 phase 等于制造下一条「文档与实况分岔」。补齐需要产品/架构 owner 给出取值,跟踪在 #2258。

## 使用规则

1. 新 REST 接口先改 `api/openapi.yaml`。
Expand Down
4 changes: 3 additions & 1 deletion api/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ ID 是字符串,使用语义前缀,不暴露数据库自增主键,创建
GET /v1/threads?projectId=proj_1&pageSize=50&pageCursor=cursor_abc
```

响应包含 `items` 和 `page.nextCursor` / `page.hasMore`。`pageSize` 默认 `50`,最大 `200`。不要把 offset pagination 作为主方式,避免消息流和事件流错位。
响应包含 `items` 和 `page.nextCursor` / `page.hasMore`。`pageSize` 默认 `50`,最大 `200`。不要把 offset pagination 作为主方式,避免消息流和事件流错位。**超过上限的 `pageSize` 是「夹取」而不是「报错」**(ADR-031 / #2243):请求值大于该端点自己声明、且被查询层实际执行的上限(通用列表 `MaxListPageSize = 200`、消息/通知族 `MaxMessagePageLimit = 100`、run/team-event 与文档族 `MaxPageLimit = 500`)时,服务端返回**上限条数** + HTTP 200 + 照常的 `page.nextCursor` / `page.hasMore`,余下部分跟着游标继续取;limit/offset 形态的端点(如通知列表)则继续推进 `offset`。它**不会**回落到默认值(那会把页二次缩短),也**不会**返回 400 —— 被夹短的页是可续取的,不是数据丢失,把它变成硬失败对调用方零收益。夹取由 `hub-server/internal/config/paging.go` 的 `ClampPageSize` 单点执行,所有 handler 侧列表都走它;实测口径见 #2243(13 个 handler 回传游标,另两个 clamp 端点是 limit/offset 形态)。

### 三种列表响应形状

Expand Down Expand Up @@ -151,6 +151,8 @@ P0 本地模式可以先实现为单用户,但文档中的权限边界必须
事件:会触发哪些 WebSocket event
```

两个取值域是**闭合**的(ADR-030 / #2258)。`x-agenthub-owner ∈ {Hub, Edge}`:早期的 `Runner` 已退役 —— `edge-server/internal/runners/` 只剩 `registry.go`,而 workspace 的 handler/service/repository/model 全在 `hub-server`、以 `/web/projects*` 暴露,照 `Runner` 找实现的人会在 Edge 找不到 workspace 逻辑进而新起第二套实现(正是「禁止在另一层再分叉实现」要防的事);workspace 的**元数据与列举归 Hub**,将来若真需要 Edge 侧文件内容端点,届时那个端点自己标 `owner: Edge`,不要预建。`x-agenthub-phase ∈ {P0, P1, P2, P3, P4}` **只适用于 `/v1/**` 设计面**:reality-face(`/client`、`/web`、`/edge`)由 `x-agenthub-status` 治理、不带 phase,`/health`、`/api`、`/cloud` 属 ops/非设计面同样不带。端点级 `x-agenthub-status ∈ {implemented, planned}`,schema 级另允许 `contract-draft`。

`api/openapi.yaml` 使用扩展字段:

```yaml
Expand Down
19 changes: 15 additions & 4 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ paths:
summary: Get workspace.
x-agenthub-phase: P0
x-agenthub-status: planned
x-agenthub-owner: Runner
x-agenthub-owner: Hub
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
Expand All @@ -1212,7 +1212,7 @@ paths:
summary: List workspace files.
x-agenthub-phase: P0
x-agenthub-status: planned
x-agenthub-owner: Runner
x-agenthub-owner: Hub
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
Expand All @@ -1227,7 +1227,7 @@ paths:
description: File path is passed in the JSON body so slash-containing paths do not become ambiguous URL segments.
x-agenthub-phase: P0
x-agenthub-status: planned
x-agenthub-owner: Runner
x-agenthub-owner: Hub
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
Expand All @@ -1241,7 +1241,7 @@ paths:
summary: Search workspace files.
x-agenthub-phase: P1
x-agenthub-status: planned
x-agenthub-owner: Runner
x-agenthub-owner: Hub
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
Expand Down Expand Up @@ -7656,6 +7656,17 @@ components:
name: pageSize
in: query
required: false
# Over-ceiling is clamped, not rejected: a request above the maximum the
# endpoint declares comes back with `maximum` items, HTTP 200 and the
# usual page.nextCursor / page.hasMore, so the rest of the collection is
# reached by following the cursor (limit/offset endpoints: by advancing
# offset). It never falls back to `default`, which would shorten the page
# a second time. See api/conventions.md §Pagination and
# hub-server/internal/config/paging.go (ADR-031 / #2243).
description: >-
Page size. Defaults to 50. Values above the endpoint's declared maximum
are clamped to that maximum (not rejected, and not reduced to the
default); follow page.nextCursor to read the rest.
schema:
type: integer
minimum: 1
Expand Down
11 changes: 7 additions & 4 deletions hub-server/internal/config/paging.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ package config
// larger batches than the interactive lists. An earlier revision of this comment
// said "three legitimate maxima" and was contradicted by the fourth.
//
// Every handler-side list clamp now goes through this function. The one
// hand-written branch left anywhere on this path is repository/message.go
// GetMessagesIncrement, whose non-positive case falls to the maximum instead of
// to def — the opposite of the rule documented above — and is tracked in #2243.
// Every clamp on this path now goes through this function, including
// repository/message.go GetMessagesIncrement, which used to be the one
// hand-written branch left. Its non-positive case falls to the maximum rather
// than to def, which looks like the opposite of the rule above but is the
// "endpoint treats 0 as no explicit limit" escape hatch documented below: it now
// spells that by passing the maximum as def, so the behaviour is unchanged and
// the shape is no longer a private copy (#2243).
//
// A caller passes the ceiling its own endpoint declares in api/openapi.yaml and
// its own query layer enforces; passing a higher one moves the clamp down a
Expand Down
11 changes: 8 additions & 3 deletions hub-server/internal/repository/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,14 @@ func GetMessagesBySession(db *gorm.DB, sessionID string, beforeSeq int64, limit
}

func GetMessagesIncrement(db *gorm.DB, sessionID string, afterSeq int64, limit int) ([]model.Message, error) {
if limit <= 0 || limit > config.MaxIncrementalMessageLimit {
limit = config.MaxIncrementalMessageLimit
}
// 0 on this endpoint means "no explicit limit" rather than "give me the
// default page", so the requested value is passed as the default too — the
// escape hatch config.ClampPageSize documents (same shape as
// handler/agent.go's runEventFilterFromQuery). Behaviour is identical to the
// hand-written branch this replaces (<=0 and >max both land on the maximum);
// routing it through the shared helper is what removes the last hand-written
// clamp on the pagination path (#2243).
limit = config.ClampPageSize(limit, config.MaxIncrementalMessageLimit, config.MaxIncrementalMessageLimit)
var msgs []model.Message
err := db.Where("session_id = ? AND seq_id > ?", sessionID, afterSeq).
Order("seq_id ASC").Limit(limit).Find(&msgs).Error
Expand Down
4 changes: 2 additions & 2 deletions scripts/verify/tests/verify-doc-entrypoints.Tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,10 @@ def assert_readme_maturity_parity(self):
with open(readme_path, "rb") as handle:
original_bytes = handle.read()
original_text = original_bytes.decode("utf-8")
if "Mobile 装配中" not in original_text:
if "Mobile 是装配中的" not in original_text:
self.fail("README fixture precondition missing zh maturity marker")
try:
mutated = original_text.replace("Mobile 装配中", "Mobile 已就绪", 1)
mutated = original_text.replace("Mobile 是装配中的", "Mobile 是已就绪的", 1)
with open(readme_path, "w", encoding="utf-8", newline="\n") as handle:
handle.write(mutated)
self.assert_failure_code("DOC-README-PARITY", "README zh-only maturity change")
Expand Down
2 changes: 1 addition & 1 deletion scripts/verify/verify-doc-ssot.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ def check_readme_parity() -> None:
Each pair below must appear in both READMEs or in neither.
"""
pairs = [
("Mobile 装配中", "Mobile in assembly"),
("Mobile 是装配中的", "Mobile is an in-assembly"),
("Desktop/Web 主线", "Desktop/Web are the mainline"),
]
readme_zh = read_text("README.md")
Expand Down
Loading