8.9 KiB
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 | 主键 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 零改动)
{ 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 抛错)
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 随后补)。app/setup.ts resolve():github 配置为空(无 repository 且无 cli)→ gitea 路径;否则 GitHub 路径 不变。Gitea 路径:resolveGiteaDeployment;!github.cli && gitea时writeEnvironment分支:被禁命令的 shims (消息 "act as on Gitea")、无 gh shim、无 github.com 凭据段;verifyEnvironment对 gitea-only 跳过(凭据检查是 github.com 特有的)。checkAccounts:source:gitea阻塞 → resolveGiteaDeployment(位置 'gitea',让无关错误不阻塞保存)。src/setup.tsPROBLEMS:加 gitea 码(位置 'gitea'):giteaUrlUnset、giteaRepositoryUnset、giteaTokenMissing、 giteaTokenInvalid、giteaRepositoryUnreadable、giteaRepositoryReadOnly。- 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 字面量)。 - 构建接线:tsdown entry
'gitea-watch': 'src/sources/gitea.ts';package.json exports"./gitea-watch"; cordis.patch.yml bundle 插入dsh-superagent-gitea名dsh-superagent/gitea-watchdisabled: true (router profile 启用它;web profile 必须不启)。007-架构.md 模块+测试表(tests/docs.spec.ts 强制完整性)+ 001-领域模型 "哪些是可复用的" 行。测试: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 + 仓库)。