113 lines
8.9 KiB
Markdown
113 lines
8.9 KiB
Markdown
# Gitea Source — 设计与决策(工作文档)
|
||
|
||
目标按 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。
|
||
|
||
## 领域映射(手册第 1 步)
|
||
|
||
| 概念 | Gitea 对应 |
|
||
| --- | --- |
|
||
| 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** |
|
||
|
||
## 配置 schema(存于 Settings.sources.gitea.config——通用 record,核心 schema 零改动)
|
||
|
||
```ts
|
||
{ url: '', // base,不带末尾斜杠:^https?://[^/]+$
|
||
repository: '', // owner/name
|
||
tokenEnv: 'GITEA_TOKEN', // 存 API token 的环境变量「名字」;token 本身绝不落盘(模式:gh 的凭据也不进配置)
|
||
pollSeconds: 60, failuresBeforeError: 3,
|
||
observeIssues: false, observePulls: false, observeComments: false }
|
||
```
|
||
Token 来源:orchestrator 宿主进程里的 `process.env[tokenEnv]`(DSH 把 `~/.dsh/.credentials.yaml` 的键注入
|
||
进程环境 → 在那里加 `GITEA_TOKEN`)。注意:DSH 会从 *worker* 子进程剥掉凭据形状的环境变量名
|
||
(infra/environment.ts 头注释)——无所谓,MVP 里 worker 从来不需要 token。
|
||
|
||
## 已验活的端点(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};头 `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`。
|
||
|
||
## 轮询流(镜像 github.ts;MVP 子集)
|
||
|
||
`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']`。
|
||
|
||
## 通道发送(幂等,GitHub 模式)
|
||
|
||
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(研判上下文)
|
||
|
||
`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 部分
|
||
|
||
`allowed: [{id: login}]` 正则 `^[A-Za-z0-9._-]+$`;可选 `bots` 同形状(缺 is_bot 的替代——
|
||
文档提示:自动化账号列在这里)。`explicit`:"名单里的账号在 Issue 或评论里 @ 智能体的账号。"
|
||
`rows`:issues/pulls/comments 行,同 github.ts(单元格来自 observe* 开关)。无 `sync`(自建,没有名册)。
|
||
|
||
## 环境变量(本种类注册)
|
||
|
||
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 变量。
|
||
|
||
## 核心补丁(必需:`configured()` 硬性要求 gh——app/setup.ts resolve() → resolveDeployment 抛错)
|
||
|
||
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 裁剪线(M2 在后):Gitea PR 观察 + threadClosed/bindThread + 自动合并;行内评审评论;
|
||
CI(branch + PR checks)trigger;Gitea 宿主的 agent 凭据 git helper(MVP worker 用机器的
|
||
环境 git 认证 = 用户自己的账号做 push;agent 身份只作用于 API 写);提到 gh 的提示词种子。
|
||
|
||
## 测试计划顺序
|
||
翻译 → 范围过滤 → round(存储/过滤/游标/去重)→ mention explicit → send sent/gone/throw →
|
||
threadContext 翻页 → apply 注册 + setupPending → policy 行 → 对 gitea.bitnasdaq.vip 的实机冒烟(需要用户:
|
||
agent 账号 + token + 仓库)。
|