Docs: translate GITEA-DESIGN.md to Chinese

This commit is contained in:
2026-11-03 19:48:13 +08:00
parent 5e4cb977e8
commit 526089a899
+84 -84
View File
@@ -1,112 +1,112 @@
# Gitea Source — design & decisions (working doc)
# Gitea Source — 设计与决策(工作文档)
Goal per docs/008 Cookbook: new Source kind `gitea` (plugin `dsh-superagent-gitea`, files `src/sources/gitea*.ts`)
for the user's self-hosted **Gitea 1.25.4 at https://gitea.bitnasdaq.vip** (OpenAPI verified against the live
instance; dump at D:/Code/AI/gitea-spec.json). Gitea's REST API deliberately mirrors GitHub's shape
(`/notifications` + `X-Poll-Interval` header, Issue has `pull_request` meta, numbers shared Issue/PR sequence),
so `gitea.ts` mirrors `github.ts` structure with a direct HTTPS fetch client instead of the `gh` CLI.
目标按 docs/008 手册:新的 Source 种类 `gitea`(插件 `dsh-superagent-gitea`,文件 `src/sources/gitea*.ts`),
服务于用户自建的 **Gitea 1.25.4 @ https://gitea.bitnasdaq.vip**(OpenAPI 已对照实机核验;dump 在
D:/Code/AI/gitea-spec.json)。Gitea 的 REST API 刻意镜像 GitHub 的形状(`/notifications` + `X-Poll-Interval`
头,Issue 带 `pull_request` 元数据,Issue/PR 共用编号序列),所以 `gitea.ts` 镜像 `github.ts` 的结构,
用直接 HTTPS fetch 客户端代替 `gh` CLI。
## Domain map (cookbook step 1)
## 领域映射(手册第 1 步)
| Concept | Gitea mapping |
| 概念 | Gitea 对应 |
| --- | --- |
| Trigger | Issue body `key('gitea-issue', n)` (same format for PRs — shared number sequence); comment/review `key('gitea-comment', url||thread, id, updated_at)` |
| Thread | `String(number)` primary; full key `gitea:<number>` |
| Actor | `user.login` (allowed list names logins). Gitea 1.25.4 API has **no `is_bot` field** → `human: true` unless actor is the agent or on the Policy `bots` list (bots list is the lever). Muted/mirrored authors: `user` may be null → login `ghost`, id ''. |
| addressed | `mentions(body, login)` — same `@login` regex as github-trigger.ts |
| Channel address | `{ number, url }` — `url` = html_url of comment/issue, ALWAYS stored (cookbook: put every needed fact in the address) |
| Receiving scope rows | `issues` / `pulls` / `comments` (switches `observeIssues`/`observePulls`/`observeComments`, all default false). CI + inline review comments + PR-close binding = **M2, not MVP** |
| Trigger | Issue 正文 `key('gitea-issue', n)`(PR 同格式——共用编号序列);评论/评审 `key('gitea-comment', url||thread, id, updated_at)` |
| Thread | 主键 `String(number)`;完整 key `gitea:<number>` |
| Actor | `user.login`(允许名单按 login 匹配)。Gitea 1.25.4 API **没有 `is_bot` 字段** → 除非 actor 是 agent 自己在 Policy `bots` 名单里,否则 `human: true`(bots 名单就是补偿手段)。静音/镜像作者:`user` 可能为 null → login `ghost`,id ''。 |
| addressed | `mentions(body, login)`——与 github-trigger.ts 相同的 `@login` 正则 |
| 通道地址 | `{ number, url }`——`url` = 评论/issue 的 html_url,**永远存下来**(手册:地址里放齐所有需要的事实) |
| 接收流 | `issues` / `pulls` / `comments`(开关 `observeIssues`/`observePulls`/`observeComments`,全默认 false)。CI + 行内评审评论 + PR 关闭绑定 = **M2,不在 MVP** |
## Config schema (stored in Settings.sources.gitea.config — generic record, zero core schema change)
## 配置 schema(存于 Settings.sources.gitea.config——通用 record,核心 schema 零改动)
```ts
{ url: '', // base, no trailing slash: ^https?://[^/]+$
{ url: '', // base,不带末尾斜杠:^https?://[^/]+$
repository: '', // owner/name
tokenEnv: 'GITEA_TOKEN', // NAME of env var holding the API token; token NEVER persisted (pattern: gh keeps creds out of config)
tokenEnv: 'GITEA_TOKEN', // 存 API token 的环境变量「名字」;token 本身绝不落盘(模式:gh 的凭据也不进配置)
pollSeconds: 60, failuresBeforeError: 3,
observeIssues: false, observePulls: false, observeComments: false }
```
Token source: `process.env[tokenEnv]` in the **orchestrator host process** (DSH injects `~/.dsh/.credentials.yaml`
keys into process env → add `GITEA_TOKEN` there). Note: DSH strips credential-shaped env names from *worker*
children (infra/environment.ts header comment) — irrelevant, workers never need the token in MVP.
Token 来源:orchestrator 宿主进程里的 `process.env[tokenEnv]`(DSH 把 `~/.dsh/.credentials.yaml` 的键注入
进程环境 → 在那里加 `GITEA_TOKEN`)。注意:DSH 会从 *worker* 子进程剥掉凭据形状的环境变量名
(infra/environment.ts 头注释)——无所谓,MVP 里 worker 从来不需要 token。
## Verified live endpoints (1.25.4)
## 已验活的端点(1.25.4)
- `GET /api/v1/notifications?all&since&limit&page` → `NotificationThread[]` {id, unread, repository{full_name},
subject{type,url,latest_comment_url,html_url,title,state}, updated_at}; header `X-Poll-Interval`.
- `GET /user` → {login,id}; `GET /repos/{o}/{r}` → {full_name, permissions{pull,push}}.
- `GET /repos/{o}/{r}/issues` (issues only), `/pulls` (PRs only) — unlike GitHub where /issues includes PRs!
Client-side filter updated_at >= cursor (server `since` support not trusted).
- `GET /repos/{o}/{r}/issues/{index}` → Issue with `pull_request` non-null when PR (same dual read as GitHub).
- `GET|POST /repos/{o}/{r}/issues/{index}/comments` (no `since` param — client filter on updated_at; pagination per_page=100&page=N until short page).
- `GET /repos/{o}/{r}/pulls/{index}/reviews` → PullReview[] {id,state,body,commit_id,user,html_url,submitted_at,updated_at}.
- **No repo-wide review-comment stream** (`/pulls/comments` MISSING in 1.25.4) → MVP review intake only via per-PR reads.
- CI for M2: `/repos/{o}/{r}/branches/{branch}` (user_can_push!), `/commits/{ref}/status` (CombinedStatus), `/actions/runs`.
subject{type,url,latest_comment_url,html_url,title,state}, updated_at};头 `X-Poll-Interval`。
- `GET /user` → {login,id};`GET /repos/{o}/{r}` → {full_name, permissions{pull,push}}。
- `GET /repos/{o}/{r}/issues`(只有 issue)、`/pulls`(只有 PR)——与 GitHub 不同(GitHub 的 /issues 包含 PR)!
客户端过滤 updated_at >= cursor(不信任服务端 `since`)。
- `GET /repos/{o}/{r}/issues/{index}` → PR 时 `pull_request` 非 null 的 Issue(与 GitHub 同一种双读)。
- `GET|POST /repos/{o}/{r}/issues/{index}/comments`(无 `since` 参数——客户端按 updated_at 过滤;分页 per_page=100&page=N 直到短页)。
- `GET /repos/{o}/{r}/pulls/{index}/reviews` → PullReview[] {id,state,body,commit_id,user,html_url,submitted_at,updated_at}。
- **没有仓库级评审评论流**(1.25.4 缺 `/pulls/comments`)→ MVP 的评审接收只走逐 PR 读取。
- M2 的 CI:`/repos/{o}/{r}/branches/{branch}`(有 user_can_push!)、`/commits/{ref}/status`(CombinedStatus)、`/actions/runs`。
## Poll flow (mirrors github.ts; MVP subset)
## 轮询流(镜像 github.ts;MVP 子集)
`pollRound` = 1) `pollNotifications` (KV gitea-since + per-number cursor KV `gitea-cursor:<n>` + retry map
`gitea-retry`; subject.url parsed by `giteaAddress` → read issue+comments[+reviews if PR]) →
2) `pollRepository` streams issues/pulls/comments (KV `gitea-stream:<id>`, cursorAt −5 s overlap, stable ids dedup,
horizon POLL_HORIZON_MS, `started` per kind) → 3) `pollThreads` subscribed threads via poll.ts `pollThreads`
(skip PR-close observation). 5 s test-timer-friendly `pause`. 3 consecutive round failures → health error
(`failuresBeforeError`). Own messages never Triggers. Health entry: `['gitea']`.
`pollRound` = 1) `pollNotifications`(KV gitea-since + 按编号的游标 KV `gitea-cursor:<n>` + 重试表
`gitea-retry`;subject.url 由 `giteaAddress` 解析 → 读 issue+评论[PR 时加 reviews])→
2) `pollRepository` 流式读 issues/pulls/comments(KV `gitea-stream:<id>`,cursorAt −5 s 重叠,稳定 id 去重,
horizon POLL_HORIZON_MS,每 kind 记 `started`)→ 3) `pollThreads` 订阅的 thread 走 poll.ts 的 `pollThreads`
(跳过 PR 关闭观察)。带 5 s 测试友好的 `pause`。连续 3 轮失败 → 健康错误
(`failuresBeforeError`)。自己的消息永远不是 Trigger。健康条目:`['gitea']`。
## Channel send (idempotent, GitHub pattern)
## 通道发送(幂等,GitHub 模式)
Marker `<!-- dsh-superagent:<message.id> -->` appended; before POST, list issue comments and if any comment by the
agent login contains the marker → 'sent'. POST `issues/{n}/comments` {body}. 'gone' when: GET issue → 404 (deleted) or
POST 403 `/locked/i`. Other throws → Outbox retries 5→300 s. `link()` = {url: address.url, label `Gitea #n`}.
`allowed(address, config)`: number parses && config.repository non-empty (address always has its own url).
marker `<!-- dsh-superagent:<message.id> -->` 追加;POST 前列出 issue 评论,若 agent login 的某条评论已含
marker → 'sent'。POST `issues/{n}/comments` {body}。'gone' 的情形:GET issue → 404(已删)或
POST 403 `/locked/i`。其它抛错 → Outbox 5→300 s 重试。`link()` = {url: address.url, label `Gitea #n`}。
`allowed(address, config)`:number 可解析 && config.repository 非空(地址永远有自己的 url)。
## threadContext (assessments)
## threadContext(研判上下文)
`messageId(trigger)`: issue body trigger → `${number}-body`; comment/review → `c${id}`/`r${id}`.
`read(part)`: fetch issue + all comments (page loop); messages = [body `${title}\n\n${body}`] + comments (+reviews
when PR and part allows), ContextMessage {id, from, at: updated_at??created_at, text, url}; 'root' → just body
message; flat forum — 'parent' returns the page containing the message (no threading in Gitea issue comments);
`next` = page cursor `p2` etc. body payload = the messages.
`messageId(trigger)`:issue 正文 trigger → `${number}-body`;评论/评审 → `c${id}`/`r${id}`。
`read(part)`:fetch issue + 全部评论(翻页循环);messages = [正文 `${title}\\n\\n${body}`] + 评论(PR 且 part 允许时
加 reviews),ContextMessage {id, from, at: updated_at??created_at, text, url};'root' → 只有正文
消息;论坛是平的——'parent' 返回包含该消息的那一页(Gitea issue 评论没有线程结构);
`next` = 页游标 `p2` 等。body payload = 这些消息。
## Policy part
## Policy 部分
`allowed: [{id: login}]` regex `^[A-Za-z0-9._-]+$`; optional `bots` same shape (the substitute for missing is_bot —
documented hint: list automation accounts here). `explicit`: "名单里的账号在 Issue 或评论里 @ 智能体的账号。".
`rows`: issues/pulls/comments rows like github.ts (cells from observe* flags). No `sync` (self-hosted, no rosters).
`allowed: [{id: login}]` 正则 `^[A-Za-z0-9._-]+$`;可选 `bots` 同形状(缺 is_bot 的替代——
文档提示:自动化账号列在这里)。`explicit`:"名单里的账号在 Issue 或评论里 @ 智能体的账号。"
`rows`:issues/pulls/comments 行,同 github.ts(单元格来自 observe* 开关)。无 `sync`(自建,没有名册)。
## Environment variables (registered by this kind)
## 环境变量(本种类注册)
DSH_SUPERAGENT_GITEA_REPOSITORY (owner/name), DSH_SUPERAGENT_GITEA_URL (base url), DSH_SUPERAGENT_GITEA_ACCOUNT
(agent login — from deployment.gitea), DSH_SUPERAGENT_GITEA_NUMBER (issue/PR number that started ticket, else '').
No token vars in worker env ever.
DSH_SUPERAGENT_GITEA_REPOSITORY (owner/name)、DSH_SUPERAGENT_GITEA_URL (base url)、DSH_SUPERAGENT_GITEA_ACCOUNT
(agent login——来自 deployment.gitea)、DSH_SUPERAGENT_GITEA_NUMBER (开出工单的 issue/PR 编号,没有则 '')。
worker 环境里永远没有 token 变量。
## Core patch (required: `configured()` hard-requires gh — app/setup.ts resolve() → resolveDeployment throws)
## 核心补丁(必需:`configured()` 硬性要求 gh——app/setup.ts resolve() → resolveDeployment 抛错)
1. `deployment.ts`: add `GiteaSetupSchema = {repository,url,tokenEnv}`, `SETUP_FIELDS.gitea`, `Deployment.gitea?:
{url, login}`; `resolveGiteaDeployment(machine, config, fetchImpl, env)`: url→GET /user (token env), repo→GET
/repos (canonical full_name + permissions.push required). Gitea-only Deployment shape:
`{ ...machine, repository: <gitea repo>, github: {cli:'',login:'',noreply:''}, gitea: {...} }` — github source
disabled in that profile, its readers never run (verified consumers: github.ts env/facts/poll only when enabled;
githubLink via registry per kind; writeEnvironment/verifyEnvironment patched next).
2. `app/setup.ts resolve()`: if github config empty (no repository && no cli) → gitea path; else GitHub path
unchanged. Gitea path: resolveGiteaDeployment; `writeEnvironment` branch when `!github.cli && gitea`: shims for
blocked commands (message "act as <gitea login> on Gitea"), NO gh shim, NO github.com credential section;
`verifyEnvironment` skipped for gitea-only (credential check is github.com-specific).
3. `checkAccounts`: block `source:gitea` → resolveGiteaDeployment (place 'gitea' so unrelated errors don't block saves).
4. `src/setup.ts` PROBLEMS: add gitea codes (place 'gitea'): giteaUrlUnset, giteaRepositoryUnset, giteaTokenMissing,
giteaTokenInvalid, giteaRepositoryUnreadable, giteaRepositoryReadOnly.
5. KV (`src/kv.ts`): `gitea: { since: record('gitea-since'), retries: record<Record<string,Retry>>('gitea-retry'),
cursor: family('gitea-cursor:'), stream: family('gitea-stream:') }` (tests/store.spec.ts requires key literals only there).
6. Build wiring: tsdown entry `'gitea-watch': 'src/sources/gitea.ts'`; package.json exports `"./gitea-watch"`;
cordis.patch.yml bundle insert `dsh-superagent-gitea` name `dsh-superagent/gitea-watch` **disabled: true**
(the router profile enables it; web profile must not). 007-architecture.md module+test tables (tests/docs.spec.ts
enforces completeness) + 001-domain "What is reusable" row. Tests: `tests/gitea.spec.ts` (fetch mocked via
injected fetchImpl / vi.stubGlobal), pattern of tests/sources.spec.ts.
1. `deployment.ts`:加 `GiteaSetupSchema = {repository,url,tokenEnv}`、`SETUP_FIELDS.gitea`、`Deployment.gitea?:
{url, login}`;`resolveGiteaDeployment(machine, config, fetchImpl, env)`:url→GET /user(token env)、repo→GET
/repos(规范 full_name + 要求 permissions.push)。Gitea-only 的 Deployment 形状:
`{ ...machine, repository: <gitea 仓库>, github: {cli:'',login:'',noreply:''}, gitea: {...} }`——该 profile 里
github source 禁用,其读者永不运行(已验消费方:github.ts 的 env/facts/poll 只在 enabled 时跑;
githubLink 走 registry 按 kind;writeEnvironment/verifyEnvironment 随后补)。
2. `app/setup.ts resolve()`:github 配置为空(无 repository 且无 cli)→ gitea 路径;否则 GitHub 路径
不变。Gitea 路径:resolveGiteaDeployment;`!github.cli && gitea` 时 `writeEnvironment` 分支:被禁命令的 shims
(消息 "act as <gitea login> on Gitea")、无 gh shim、无 github.com 凭据段;
`verifyEnvironment` 对 gitea-only 跳过(凭据检查是 github.com 特有的)。
3. `checkAccounts`:`source:gitea` 阻塞 → resolveGiteaDeployment(位置 'gitea',让无关错误不阻塞保存)。
4. `src/setup.ts` PROBLEMS:加 gitea 码(位置 'gitea'):giteaUrlUnset、giteaRepositoryUnset、giteaTokenMissing、
giteaTokenInvalid、giteaRepositoryUnreadable、giteaRepositoryReadOnly。
5. KV(`src/kv.ts`):`gitea: { since: record('gitea-since'), retries: record<Record<string,Retry>>('gitea-retry'),
cursor: family('gitea-cursor:'), stream: family('gitea-stream:') }`(tests/store.spec.ts 只在那里要求 key 字面量)。
6. 构建接线:tsdown entry `'gitea-watch': 'src/sources/gitea.ts'`;package.json exports `"./gitea-watch"`;
cordis.patch.yml bundle 插入 `dsh-superagent-gitea` 名 `dsh-superagent/gitea-watch` **disabled: true**
(router profile 启用它;web profile 必须不启)。007-architecture.md 模块+测试表(tests/docs.spec.ts
强制完整性)+ 001-domain "What is reusable" 行。测试:`tests/gitea.spec.ts`(fetch 用注入的
fetchImpl / vi.stubGlobal mock),模式同 tests/sources.spec.ts。
## MVP cut lines (M2 later): Gitea PR observation + threadClosed/bindThread + auto-merge; inline review comments;
CI (branch + PR checks) triggers; agent-credential git helper for the Gitea host (MVP workers use the machine's
ambient git auth = user's own account for pushes; agent identity applies to API writes); prompt seeds that mention gh.
## MVP 裁剪线(M2 在后):Gitea PR 观察 + threadClosed/bindThread + 自动合并;行内评审评论;
CI(branch + PR checks)trigger;Gitea 宿主的 agent 凭据 git helper(MVP worker 用机器的
环境 git 认证 = 用户自己的账号做 push;agent 身份只作用于 API 写);提到 gh 的提示词种子。
## Test plan order
translate → scope filtered → round (stored/filtered/cursor/dup) → mention explicit → send sent/gone/throw →
threadContext pages → apply registers + setupPending → policy rows → live smoke vs gitea.bitnasdaq.vip (needs user:
agent account + token + repo).
## 测试计划顺序
翻译 → 范围过滤 → round(存储/过滤/游标/去重)→ mention explicit → send sent/gone/throw →
threadContext 翻页 → apply 注册 + setupPending → policy 行 → 对 gitea.bitnasdaq.vip 的实机冒烟(需要用户:
agent 账号 + token + 仓库)。