Compare commits
24 Commits
6c565f0ffe
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fb46385e2 | ||
|
|
5e9bda7984 | ||
|
|
ac00f4e67d | ||
|
|
f8f5ef3a0b | ||
|
|
db04e088bc | ||
|
|
9242e79520 | ||
|
|
96bfcaaf99 | ||
|
|
8ecc778645 | ||
|
|
c38c937c05 | ||
|
|
a1e66c08f1 | ||
|
|
b1cd820819 | ||
|
|
e785e3c5dd | ||
|
|
d9db66ca8f | ||
|
|
965aefe9a4 | ||
|
|
b8fcc12229 | ||
|
|
6fff9023b2 | ||
|
|
97b4a32dda | ||
|
|
e62a545569 | ||
|
|
f15bc92d1e | ||
|
|
05480e511d | ||
|
|
bd20a79d4b | ||
|
|
dd233a40e8 | ||
|
|
265199a48e | ||
|
|
17a924fd47 |
88
.ccweb/scripts/conversation-idle-listener-e2e-20260827.js
Normal file
88
.ccweb/scripts/conversation-idle-listener-e2e-20260827.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
getCurrentConversationId,
|
||||||
|
createConversation,
|
||||||
|
sendMessage,
|
||||||
|
getLastMessage,
|
||||||
|
getConversationStatus,
|
||||||
|
getChildConversationIds,
|
||||||
|
onConversationEvent,
|
||||||
|
} from '@ccweb/session';
|
||||||
|
|
||||||
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
const sourceConversationId = await getCurrentConversationId();
|
||||||
|
const conversationId = await createConversation(
|
||||||
|
'这是会话事件测试,请只回复:测试对话已准备'
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialStatus = await getConversationStatus(conversationId);
|
||||||
|
const childrenBefore = await getChildConversationIds(sourceConversationId);
|
||||||
|
if (!childrenBefore.includes(conversationId)) {
|
||||||
|
throw Object.assign(new Error('新对话未出现在来源对话的直接子对话 ID 中'), {
|
||||||
|
code: 'child_conversation_missing',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
let continued = false;
|
||||||
|
let continuationMessage = '';
|
||||||
|
let unsubscribe = () => {};
|
||||||
|
let finish;
|
||||||
|
let fail;
|
||||||
|
const completed = new Promise((resolve, reject) => {
|
||||||
|
finish = resolve;
|
||||||
|
fail = reject;
|
||||||
|
});
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
unsubscribe();
|
||||||
|
fail(Object.assign(new Error('等待 idle 自动继续闭环超时'), {
|
||||||
|
code: 'idle_listener_timeout',
|
||||||
|
}));
|
||||||
|
}, 180000);
|
||||||
|
|
||||||
|
unsubscribe = await onConversationEvent(conversationId, 'idle', async (event) => {
|
||||||
|
const lastMessage = await getLastMessage(conversationId);
|
||||||
|
events.push({ ...event, lastMessage });
|
||||||
|
|
||||||
|
if (!continued && lastMessage.includes('未完成')) {
|
||||||
|
continued = true;
|
||||||
|
continuationMessage = await sendMessage(
|
||||||
|
conversationId,
|
||||||
|
'请继续完成任务,并且只回复:任务完成'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (continued && lastMessage.includes('任务完成')) {
|
||||||
|
unsubscribe();
|
||||||
|
clearTimeout(timeout);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstTurnPromise = sendMessage(
|
||||||
|
conversationId,
|
||||||
|
'开始执行第一阶段,并且只回复:未完成'
|
||||||
|
);
|
||||||
|
await delay(150);
|
||||||
|
const statusDuringFirstTurn = await getConversationStatus(conversationId);
|
||||||
|
const firstMessage = await firstTurnPromise;
|
||||||
|
await completed;
|
||||||
|
|
||||||
|
const finalStatus = await getConversationStatus(conversationId);
|
||||||
|
const finalMessage = await getLastMessage(conversationId);
|
||||||
|
const childrenAfter = await getChildConversationIds(sourceConversationId);
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
sourceConversationId,
|
||||||
|
conversationId,
|
||||||
|
initialStatus,
|
||||||
|
statusDuringFirstTurn,
|
||||||
|
firstMessage,
|
||||||
|
continued,
|
||||||
|
continuationMessage,
|
||||||
|
finalStatus,
|
||||||
|
finalMessage,
|
||||||
|
childIncluded: childrenAfter.includes(conversationId),
|
||||||
|
idleEventCount: events.length,
|
||||||
|
events,
|
||||||
|
}));
|
||||||
8
.ccweb/scripts/package.json
Normal file
8
.ccweb/scripts/package.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "ccweb-script-runtime",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@ccweb/session": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
.ccweb/scripts/restart-smoke-20260826.js
Normal file
4
.ccweb/scripts/restart-smoke-20260826.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { getCurrentConversationId } from '@ccweb/session';
|
||||||
|
|
||||||
|
const conversationId = await getCurrentConversationId();
|
||||||
|
console.log(JSON.stringify({ smoke: true, conversationId }));
|
||||||
31
.ccweb/scripts/session-orchestration-e2e-20260826.js
Normal file
31
.ccweb/scripts/session-orchestration-e2e-20260826.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
createConversation,
|
||||||
|
sendMessage,
|
||||||
|
selectSemanticBranch,
|
||||||
|
} from '@ccweb/session';
|
||||||
|
|
||||||
|
const conversationId = await createConversation(
|
||||||
|
'这是 JavaScript 会话编排端到端测试。请只回复:测试成功'
|
||||||
|
);
|
||||||
|
|
||||||
|
const branch = await selectSemanticBranch(conversationId, [
|
||||||
|
'测试成功',
|
||||||
|
'测试失败',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (branch !== '测试成功') {
|
||||||
|
const error = new Error(`语义分支未命中测试成功:${branch}`);
|
||||||
|
error.code = 'unexpected_semantic_branch';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await sendMessage(
|
||||||
|
conversationId,
|
||||||
|
'收到。请只回复:已收到'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
conversationId,
|
||||||
|
branch,
|
||||||
|
message,
|
||||||
|
}));
|
||||||
18
.env.example
18
.env.example
@@ -10,5 +10,23 @@ CLAUDE_PATH=claude
|
|||||||
# Codex CLI 路径(默认在 PATH 中查找 codex)
|
# Codex CLI 路径(默认在 PATH 中查找 codex)
|
||||||
CODEX_PATH=codex
|
CODEX_PATH=codex
|
||||||
|
|
||||||
|
# Codex App / ccweb MCP 启动与调用超时(可选)
|
||||||
|
# CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC=30
|
||||||
|
# CC_WEB_CODEX_MCP_TOOL_TIMEOUT_SEC=60
|
||||||
|
# CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS=35000
|
||||||
|
# CC_WEB_CODEX_APP_MCP_RELOAD_TRACK_MS=60000
|
||||||
|
|
||||||
# PushPlus Token(可选,首次启动会自动迁移到 config/notify.json)
|
# PushPlus Token(可选,首次启动会自动迁移到 config/notify.json)
|
||||||
PUSHPLUS_TOKEN=
|
PUSHPLUS_TOKEN=
|
||||||
|
|
||||||
|
# Gitea Workflow(MVP:单实例、全局 ccweb-bot)
|
||||||
|
CC_WEB_GITEA_HOST=https://gitea.example.com
|
||||||
|
CC_WEB_GITEA_INSTANCE_ID=default
|
||||||
|
CC_WEB_GITEA_BOT_LOGIN=ccweb-bot
|
||||||
|
CC_WEB_GITEA_BOT_TOKEN=
|
||||||
|
CC_WEB_GITEA_WEBHOOK_SECRET=
|
||||||
|
CC_WEB_GITEA_WORKSPACE_ROOT=/var/lib/ccweb/workspaces
|
||||||
|
# 可选:替换 cc-web 自带的 gitea-mcp;留空时自动使用 bin/gitea-mcp
|
||||||
|
# CC_WEB_GITEA_MCP_COMMAND=/opt/gitea-mcp/bin/gitea-mcp
|
||||||
|
# gitea-mcp stdio initialize 预检超时(毫秒,默认 8000,上限 30000)
|
||||||
|
CC_WEB_GITEA_MCP_STARTUP_TIMEOUT_MS=8000
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -8,6 +8,15 @@ config/notify.json
|
|||||||
config/auth.json
|
config/auth.json
|
||||||
config/model.json
|
config/model.json
|
||||||
config/codex.json
|
config/codex.json
|
||||||
|
config/gitea-workflow.json
|
||||||
|
config/gitea-workflow-management.json
|
||||||
|
config/gitea-workflow-secrets.json
|
||||||
|
config/gitea-webhook-deliveries.json
|
||||||
|
config/gitea-repositories.json
|
||||||
|
config/cross-conversation-replies.json
|
||||||
|
config/instance-icon.png
|
||||||
|
config/.instance-icon.png.tmp
|
||||||
|
gitea-workspaces/
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
dist-exe/*
|
dist-exe/*
|
||||||
!dist-exe/*.tar.gz
|
!dist-exe/*.tar.gz
|
||||||
|
|||||||
47
.planning/2026-08-23-gitea-workflow/findings.md
Normal file
47
.planning/2026-08-23-gitea-workflow/findings.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Gitea Workflow 研究与决策
|
||||||
|
|
||||||
|
## 需求
|
||||||
|
|
||||||
|
- Gitea 实例级 Webhook 指向 cc-web;用户在 Issue/PR 普通评论中 `@ccweb-bot`。
|
||||||
|
- 未登记仓库首次合法触发自动接入,clone 到可配置工作区根目录。
|
||||||
|
- 同一仓库串行、不同仓库并行;任务队列、会话映射和回执状态必须持久化。
|
||||||
|
- 所有后台会话固定使用 Codex App 与 yolo;Gitea 是唯一主交互入口。
|
||||||
|
- Agent 通过官方 `gitea-mcp` 读取上下文、研究、修改和回帖;cc-web 负责状态与最终兜底。
|
||||||
|
|
||||||
|
## 本地代码发现
|
||||||
|
|
||||||
|
- `server.js` 已有持久会话创建、`handleMessage`、Codex App 线程、MCP 配置和 turn 完成路径。
|
||||||
|
- `codexAppThreadConfig()` 已将运行时 MCP 组装成 `thread/start.config`;可扩展为工作流专属 `gitea` 配置。
|
||||||
|
- `handleCodexAppTurnComplete()` 是最合适的 turn 完成监听点。
|
||||||
|
- `handleInternalMcpApi()` 是 cc-web 内部 MCP API,不应暴露为 Gitea 公网 Webhook。
|
||||||
|
- 项目无 Node SQLite 依赖;现有会话和配置以 JSON 文件持久化,工作流状态可采用原子 JSON 文件或新增独立存储模块。
|
||||||
|
|
||||||
|
## 官方 gitea-mcp
|
||||||
|
|
||||||
|
- 官方仓库:`https://gitea.com/gitea/gitea-mcp`。
|
||||||
|
- 支持本机 stdio 与 HTTP;本任务选择 stdio。
|
||||||
|
- 支持 `GITEA_HOST`、`GITEA_ACCESS_TOKEN`,可通过 `-t stdio -H <host>` 启动。
|
||||||
|
- 工具覆盖 Issue、PR、仓库、文件、分支、提交、Release、Actions 等。
|
||||||
|
- 支持 scope/tool 过滤,但用户要求 MVP 开放全部写权限;必须配套暂停、中止和审计。
|
||||||
|
|
||||||
|
## 外部方案
|
||||||
|
|
||||||
|
- Matea:最接近完整 Gitea Webhook Agent 网关,可参考其签名、去重、队列和状态机;社区规模仍小。
|
||||||
|
- wshm:支持多 Forge 和后台同步,但不是 cc-web 会话桥接。
|
||||||
|
- pi-dispatch:队列、预算和沙箱值得参考,但使用 pi Agent。
|
||||||
|
- gitea-claude-agent:验证 `@机器人 → 多轮澄清 → PR` 交互,但依赖 Gitea Actions。
|
||||||
|
|
||||||
|
## 关键技术约束
|
||||||
|
|
||||||
|
- Webhook 必须在验签、delivery 去重、Bot 自评论过滤后才创建任务。
|
||||||
|
- 每个 turn 生成唯一标识,正常最终评论由 MCP 发送;Webhook/API 查询确认真实回帖。
|
||||||
|
- 任务完成但没有回帖时,只允许在同一会话发起一次“只补发回执、不重复修改”的隐藏 turn。
|
||||||
|
- 回执仍失败才使用 Bot Token 走 Gitea REST 兜底。
|
||||||
|
- 共享工作区有未提交修改时不 reset、不覆盖;当前会话可继续,其他会话排队。
|
||||||
|
|
||||||
|
## 已落地实现
|
||||||
|
|
||||||
|
- 主服务入口为 `lib/gitea-workflow-service.js`,底层组合 `domain/store/queue`;避免同时维护两套运行时状态。
|
||||||
|
- `server.js` 只负责实例化配置、Webhook 路由、工作区 runner、Codex App turn 桥接和管理 API。
|
||||||
|
- 管理配置中的 Secret/Token 保存到独立 0600 文件,GET 配置只返回“是否已配置”;保存后重启使运行时闭环读取新凭据。
|
||||||
|
- 回帖核验只接受 `kind=final|fallback` 的隐藏标识,状态评论不会误判为最终回帖;查询异常也只进行一次“只补发回执”补偿。
|
||||||
148
.planning/2026-08-23-gitea-workflow/integration-map.md
Normal file
148
.planning/2026-08-23-gitea-workflow/integration-map.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Gitea Workflow × Codex App 集成地图
|
||||||
|
|
||||||
|
> 分析日期:2026-08-24
|
||||||
|
>
|
||||||
|
> 范围:只读分析 Codex App 的 `thread/start` MCP 配置、会话创建、消息处理、turn 完成、后台运行、重启恢复和现有回归入口。未修改 `server.js`、`lib/`、`public/`。
|
||||||
|
|
||||||
|
## 证据与方法
|
||||||
|
|
||||||
|
- codebase-memory 项目:`home-cc-web`,`list_projects` 返回根目录 `/home/cc-web`,节点 6670、边 15041;`index_status` 为 `ready`。
|
||||||
|
- 使用 `get_architecture(aspects=["all"])` 确认 `server`、`codex-app-runtime`、`regression`、`codex` 等模块边界。
|
||||||
|
- 使用 `search_graph`、`get_code_snippet`、`trace_path(mode="calls")` 定位函数和调用关系;再以 `rg -n`/`nl -ba` 交叉核对行号。
|
||||||
|
- 图谱无法把回调赋值完整表示出来:`handleCodexAppNotification` 的调用者列表为空,但源码显示它在 `getCodexAppClient()` 作为 `onNotification` 回调传入客户端,再由 JSONL 客户端转发。因此下图的“回调边”以源码为准。
|
||||||
|
|
||||||
|
## 端到端运行链路
|
||||||
|
|
||||||
|
```text
|
||||||
|
WebSocket message (server.js:7706-7766)
|
||||||
|
-> handleMessage (server.js:9852-10190)
|
||||||
|
-> handleCodexAppMessage (server.js:11888-11960)
|
||||||
|
-> activeCodexAppTurns + persistCodexAppTurnState(immediate)
|
||||||
|
-> startCodexAppTurn (server.js:11962-12022)
|
||||||
|
-> getCodexAppClient (server.js:11782-11845)
|
||||||
|
-> createCodexAppServerClient (lib/codex-app-server-client.js:6-223)
|
||||||
|
initialize -> initialized -> postInitialize
|
||||||
|
-> thread/start 或 thread/resume (threadParams)
|
||||||
|
-> codexAppThreadConfig -> mcp_servers.* / web_search
|
||||||
|
-> turn/start (collaborationMode)
|
||||||
|
-> app-server stdout JSONL
|
||||||
|
-> client.handleMessage (lib/codex-app-server-client.js:78-111)
|
||||||
|
-> onNotification(handleCodexAppNotification)
|
||||||
|
-> findCodexAppRouteByRuntime (server.js:11050-11066)
|
||||||
|
-> codexAppRuntime.processCodexAppNotification (lib/codex-app-runtime.js:865-1040)
|
||||||
|
-> persist state; turn/completed -> handleCodexAppTurnComplete
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. 线程级 MCP 配置和环境
|
||||||
|
|
||||||
|
| 位置 | 行号 | 当前行为 | Gitea 扩展建议 |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `server.js` `buildCcwebMcpRuntimeConfig` | 2709-2753 | 生成内置 `ccweb` MCP;HTTP 模式把来源会话/跳数放入 URL,stdio 模式生成命令及 env。 | Gitea MCP 应作为同级线程配置注入,不要改 app-server 进程全局 env。 |
|
||||||
|
| `server.js` `listRuntimeMcpServerConfigs` | 2755-2772 | 合并项目 `.codex/config.toml` MCP 与内置 `ccweb`,按规范化 server 名去重。 | 增加 workflow 专属配置的明确入口(建议由 `mcpContext`/工作流配置传入),并定义与项目同名时的优先级。 |
|
||||||
|
| `server.js` `codexAppCcwebMcpEnv` | 11680-11691 | 输出 `CC_WEB_SOURCE_SESSION_ID`、`CC_WEB_CROSS_HOP_COUNT`、内部 token 等。 | Gitea 仓库/任务 ID 应随 `thread/start.config.mcp_servers.gitea.env` 下发;不要写入 `buildCodexAppClientSpec().env`。 |
|
||||||
|
| `server.js` `codexAppThreadConfig` | 11693-11702 | 把 runtime 配置映射为 `config["mcp_servers.<server>"]`,并设置 `web_search`。 | 这是最直接的 `gitea-mcp` 注入扩展点;应保证每个线程拿到独立 host/token/workflow 上下文。 |
|
||||||
|
| `server.js` `codexAppThreadParams` | 11857-11868 | 组装 cwd、model、权限和 `config`,用于 start/resume。 | Gitea 任务必须从这里同时覆盖 `thread/start` 和 `thread/resume`,避免重试/恢复丢 MCP。 |
|
||||||
|
| `server.js` `buildCodexAppClientSpec` | 11735-11780 | 构造单例 app-server 进程的 command/args/env/signature;会剥离敏感环境变量。 | 不要把仓库 token 放此处;若新增全局配置,需纳入 signature 并评估活跃 turn 的复用策略。 |
|
||||||
|
|
||||||
|
当前 `codexAppCcwebMcpEnv` 已证明“来源上下文线程级下发”可行。Gitea 方案应沿用这一模式,例如 `mcp_servers.gitea` 的 `command/args/env` 或 streamable HTTP 配置,而不是 dynamic tools。
|
||||||
|
|
||||||
|
### 2. 会话创建、turn 启动和协作参数
|
||||||
|
|
||||||
|
| 位置 | 行号 | 当前行为 | Gitea 扩展建议 |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `server.js` WebSocket 分发 | 7706-7766 | `message` 进入 `handleMessage`;slash 先经 `handleSlashCommand`。 | Webhook 不应伪造 WebSocket;工作流服务可调用同一内部处理函数或抽取“创建/投递消息”服务层。 |
|
||||||
|
| `server.js` `handleMessage` | 9852-10190 | 校验输入、创建/加载 JSON 会话、持久化用户消息;Codex App 分支调用 `handleCodexAppMessage`。`ws` 可为 null,`wsSend` 会安全忽略。 | 后台 Gitea 任务可复用会话创建逻辑,但应显式传 workflow 元数据、固定 agent=`codexapp`/yolo,并避免前端 slash 语义污染。 |
|
||||||
|
| `server.js` `handleCodexAppMessage` | 11888-11960 | 建立 `activeCodexAppTurns` entry,记录 `mcpContext`/retry 信息,立即写 state,然后异步启动 turn。 | 在 entry/retry state 中保留不可变的 Gitea workflow/task/repository correlation;该信息不能只存在内存。 |
|
||||||
|
| `server.js` `startCodexAppTurn` | 11962-12022 | 复用单例客户端;有 threadId 则 resume,否则 start;校验恢复线程 ID;保存 runtime thread ID;发送 turn/start。 | Gitea 首次任务走 start,后续评论/重试走同一 thread resume;将 gitea MCP 配置传入 `codexAppThreadParams`,并在 mismatch 时按任务失败处理。 |
|
||||||
|
| `server.js` `codexAppCollaborationMode` / `codexAppTurnParams` | 11704-11712、11870-11886 | collaboration mode 总是存在,`plan` 映射 plan,否则 default;model/reasoning_effort/developer instructions 放 settings,顶层不重复 model/effort。 | 后台固定 yolo 会得到 mode=default;不要为 Gitea 任务再传顶层 model/effort,避免回到非原生协作路径。 |
|
||||||
|
| `server.js` `codexAppPostInitialize` | 11714-11733 | initialize 后 best-effort 探测 goals 和 `collaborationMode/list`,失败只记日志。 | Gitea 集成不应依赖探测成功;可在能力缺失时记录降级并继续普通 turn。 |
|
||||||
|
|
||||||
|
### 3. JSON-RPC 客户端、通知、服务端请求
|
||||||
|
|
||||||
|
| 位置 | 行号 | 当前行为 | Gitea 扩展建议 |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `lib/codex-app-server-client.js` `handleMessage` | 78-111 | 以 `id` 区分 pending response/server request;无 `id` 的 method 交给 `onNotification`。 | 回执监听应基于 turn/thread ID,不要解析 UI 文本;保留未知 method 日志。 |
|
||||||
|
| `lib/codex-app-server-client.js` `start` | 140-190 | 启动 stdio app-server,绑定 readline,initialize/initialized,再运行 `postInitialize`;退出 reject 所有 pending。 | gitea-mcp 子进程由 app-server 按线程配置管理;不要另起一套 app-server 连接。 |
|
||||||
|
| `server.js` `getCodexAppClient` | 11782-11845 | 全局单例 `codexAppClient`;配置 signature 变化时若有其他活跃 turn 会复用旧客户端,否则失败残留并重启。 | 队列不能通过频繁改全局 config 切换仓库;每个任务使用线程 config,避免触发 singleton stale-config 分支。 |
|
||||||
|
| `server.js` `handleCodexAppNotification` | 11105-11155 | 先处理 compact/Goal/MCP startup,再按 parent/child/磁盘恢复路由;runtime 返回 done 时进入完成处理。 | Gitea 回执监听应挂在此处或其下游的领域事件,而不是监听 WebSocket `done`;需识别 `turn/completed` 的 status/error。 |
|
||||||
|
| `server.js` `handleCodexAppServerRequest` | 11572-11608 | 处理 approval、user input、dynamic tool;未知请求按保守策略拒绝。 | gitea-mcp 是 MCP server,不应新增 dynamic tool 旁路;若需要用户确认,明确映射到工作流 waiting_user。 |
|
||||||
|
| `server.js` `handleCodexAppServerExit` / `handleCodexAppTurnFailure` | 11610-11637、12165-12170 | app-server 退出会使所有 active Codex App turn 失败;失败统一进入完成清理和重试判定。 | Gitea 任务需把此类失败映射为可恢复状态,防止一次 singleton 退出同时丢失多个仓库任务。 |
|
||||||
|
|
||||||
|
### 4. turn 完成、后台运行与持久化恢复
|
||||||
|
|
||||||
|
| 位置 | 行号 | 当前行为 | Gitea 扩展建议 |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `server.js` `handleCodexAppTurnComplete` | 12024-12163 | 去重并持久化 assistant/toolCalls;处理 transient retry;删除 active entry、清理 run dir;有 ws 发 `done`,无 ws 则广播 `background_done` 并调用通知。 | 在删除 entry/清理目录前写入 workflow completion/outbox;回执确认、补触发和 REST 兜底必须幂等。建议发领域事件,避免依赖 `sendNotification`。 |
|
||||||
|
| `server.js` `handleCodexAppSteerMessage` | 12189-12419 | 运行中用 `turn/steer`;遇到 no-active-turn 会先收敛旧输出,再复用消息启动新 turn。 | Gitea 连续评论可转为 steer,但要用任务级 message/delivery ID 去重,避免 steer fallback 重复修改仓库。 |
|
||||||
|
| `server.js` `handleCodexAppAbortSession` | 12421-12451 | 发送 `turn/interrupt`,超时后强制完成;无可用 client 直接以 interrupted 完成。 | 工作流取消/仓库停用可复用,但状态机需区分用户取消、运维中止、app-server 崩溃。 |
|
||||||
|
| `server.js` `writeCodexAppTurnState` / `persistCodexAppTurnState` / `loadCodexAppTurnState` | 5039-5059、5061-5082、5093-5116 | state 原子写入、延迟 flush、大小保护;序列化当前 turn 文本/toolCalls/usage/error。 | 必须把 workflow/task/repository correlation 和回执状态纳入独立持久化(或扩展 state schema),不能只依赖 `entry.mcpContext`。 |
|
||||||
|
| `server.js` `recoverCodexAppTurnState` | 5137-5226 | 重启后恢复 partial assistant/toolCalls,写 interrupted system message,清理 run dir;不自动继续 turn。 | `queued`/`waiting_user`/`running` 的 Gitea 状态恢复应在工作流队列层实现;running 只能标为 interrupted 后按策略最多重试一次。 |
|
||||||
|
| `server.js` `recoverProcesses` + 启动调用 | 7398-7490、12950 | 启动扫描 `*-run`;Codex App state 优先走 `recoverCodexAppTurnState`,普通进程走 JSONL tail/replay。 | 工作流恢复入口应在此生命周期之后加载 durable queue,再决定是否重新投递;不要在 `recoverProcesses` 中直接执行 Gitea 网络副作用。 |
|
||||||
|
| `server.js` `updateSessionRuntimeThreadIndex` / `saveSession` | 3443-3456、4497-4520 | 保存会话时维护 thread→session O(1) 索引;未知线程有负缓存,避免通知热路径反复扫盘。 | Gitea 任务必须持久化稳定 session/thread 映射;删除仓库/会话时同步清理映射,避免跨仓库通知串线。 |
|
||||||
|
| `server.js` `findCodexAppRouteByRuntime` | 11050-11066 | parent active entry → 已知 child thread → 磁盘 adopt;child 先于 parent adoption。 | Gitea MCP 可能产生 child/collab activity,工作流回执只认 parent turn;需保留 child 事件但禁止把 child completion 当最终回执。 |
|
||||||
|
|
||||||
|
无 WebSocket 的后台路径是可行的:`handleMessage(null, ...)` 的 `wsSend` 为空安全,`handleCodexAppTurnComplete` 会走 `background_done`/通知分支。但该分支目前没有 Gitea 专属完成回调,必须增加可持久化的领域事件或 outbox。
|
||||||
|
|
||||||
|
## 建议的最小扩展边界
|
||||||
|
|
||||||
|
1. 新增独立 Gitea Workflow 服务/存储模块:Webhook 验签、delivery 去重、仓库/任务状态机、同仓库锁、全局并发上限、outbox/审计。不要把队列逻辑塞进 `handleMessage` 或 app-server 客户端。
|
||||||
|
2. 为 Codex App 调用增加“线程级 MCP 配置”参数(沿用 `mcpContext` 传递),在 `listRuntimeMcpServerConfigs` → `codexAppThreadConfig` → `codexAppThreadParams` 形成单向注入;官方 `gitea-mcp` 使用 stdio 或明确的 streamable HTTP 配置,token 只放线程 env/安全存储。
|
||||||
|
3. 将 workflow correlation(实例、owner/repo、issue/PR、评论 delivery、turn ID)写入独立任务记录,并在 turn state 中保存最小引用,保证进程重启和自动重试可恢复。
|
||||||
|
4. 在 `handleCodexAppNotification`/`handleCodexAppTurnComplete` 旁增加内部领域事件适配器:先 durable commit,再确认 Gitea 评论;缺失回执只允许一次补触发,最后 REST 兜底。
|
||||||
|
5. 后台任务通过现有 Codex App 单例运行,但由 Gitea 队列决定何时调用;不要修改 `buildCodexAppClientSpec` 的全局环境来切换仓库。
|
||||||
|
|
||||||
|
## 潜在冲突与风险
|
||||||
|
|
||||||
|
- **单例 app-server 与多仓库并发**:`getCodexAppClient` 只有一个 client,退出会影响全部 active turns;队列必须在 client 层之上做并发/重试隔离。
|
||||||
|
- **线程配置与进程环境边界**:`buildCodexAppClientSpec` 的 env 是进程级,不能承载某个仓库的 Gitea token;否则并发任务会串凭据。
|
||||||
|
- **恢复丢失 MCP 上下文**:`adoptCodexAppUnroutedTurn`(`server.js:10379-10423`)新建 entry 时 `mcpContext: {}`;`recoverCodexAppTurnState` 也只恢复输出,不恢复 workflow 上下文。必须把上下文放独立任务记录或 state schema。
|
||||||
|
- **完成清理时序**:`handleCodexAppTurnComplete` 在 12087-12088 删除 active entry/清理 run dir,之后才通知后台客户端;回执 outbox 必须在清理前落盘。
|
||||||
|
- **自动重试副作用**:`shouldRetryCodexTransientFailure` 路径会重新启动 turn;若模型已修改仓库或已发评论,需用任务/评论/turn 幂等键防止重复写入。
|
||||||
|
- **通知路由与未知线程**:当前有 O(1) index、磁盘 adoption 和负缓存;Gitea 不能只依赖内存 active map,否则重启后通知会变成 unrouted。
|
||||||
|
- **现有运行保护**:`handleMessage` 会拒绝 active turn/Goal/compaction;工作流必须先做同仓库串行和取消策略,不能靠重复调用绕过保护。
|
||||||
|
- **协作模式形状**:启用 collaborationMode 时顶层不得重复 `model`/`effort`;gitea workflow 的 yolo 固定策略应保持 `mode=default` + settings。
|
||||||
|
- **安全边界**:Webhook HMAC、Bot 自评论过滤、delivery 去重、token 脱敏、remote URL 不落 token,需要在 Gitea 层完成,不能依赖 Codex App MCP 工具自行保证。
|
||||||
|
|
||||||
|
## 必须补的测试
|
||||||
|
|
||||||
|
### Codex App 协议/单元回归
|
||||||
|
|
||||||
|
- `thread/start` 首次任务与 `thread/resume` 重试都包含 `mcp_servers.gitea`,且每个 session 的 host/token/workflow env 隔离;项目 MCP、ccweb MCP、gitea MCP 同时存在时不丢失/不误去重。
|
||||||
|
- collaborationMode 仍把 model/reasoning_effort/developer instructions 放在 `settings`,顶层没有重复 model/effort;yolo 固定为 default。
|
||||||
|
- `handleMessage(null, ...)` 能创建后台 Codex App 会话;无 ws 完成时仍持久化 assistant/toolCalls,并触发可断言的 workflow completion/outbox,而不是只依赖 push notification。
|
||||||
|
- turn state 原子写入/重启恢复包含 workflow correlation;恢复后 parent thread、gitea MCP 配置和回执状态不丢失;未知线程 adoption 不得把 child 事件当 parent 最终完成。
|
||||||
|
- app-server 退出、transient capacity/reconnect、thread resume mismatch、steer no-active-turn 均只产生一次工作流状态转移和一次回执动作。
|
||||||
|
|
||||||
|
### Gitea Workflow 业务回归
|
||||||
|
|
||||||
|
- Webhook HMAC 正确/错误、重复 delivery、Bot 自评论、非 Issue/PR 普通评论过滤。
|
||||||
|
- 首次合法触发自动接入仓库、clone/fetch 失败恢复、同仓库串行、不同仓库并行及全局并发上限。
|
||||||
|
- 评论→session 映射稳定(`gitea + owner/repo + type + number`),连续评论 steer/resume 不重复用户消息或仓库修改。
|
||||||
|
- turn 完成后 MCP 回帖确认:正常一次;缺失时仅一次“只补回执”隐藏 turn;再次失败才 REST 兜底;重启/重复 webhook 不重复评论。
|
||||||
|
- running 中断、waiting_user 保留、queued 恢复、仓库暂停/全局暂停/取消排队/中止 turn 的状态机和审计日志。
|
||||||
|
- token 不出现在日志、错误消息、session JSON、git remote URL;Webhook 重放和跨仓库 thread/child 通知不能串任务。
|
||||||
|
|
||||||
|
### 现有可复用测试入口
|
||||||
|
|
||||||
|
| 入口 | 行号 | 已覆盖内容 |
|
||||||
|
|---|---:|---|
|
||||||
|
| `scripts/regression.js` `assertCodexAppRuntimeSubAgentActivityContract` | 2276-2500 | runtime 通知到工具/子代理卡片的归一化。 |
|
||||||
|
| `scripts/regression.js` `assertCodexAppTransientReconnectContract` | 2501-2542 | reconnect 进度不结束 turn,终态错误结束 turn。 |
|
||||||
|
| `scripts/regression.js` `assertCodexAppStaleRunningRecoveryContract` | 4038-4058 | stale steer 替换 turn 的持久化/flush 约束。 |
|
||||||
|
| `scripts/regression.js` `runCodexAppRuntimeImageSteerRegression` | 4233-4365 | 后台/运行中 steer 真实 WebSocket 流程。 |
|
||||||
|
| `scripts/regression.js` `runCodexAppStaleRunningRegression` | 4367-4558 | app-server 缺失终态通知后的恢复及消息去重。 |
|
||||||
|
| `scripts/regression.js` `assertCodexAppChildToolRoutingContract` | 4867-5074 | child thread 与父卡片路由隔离。 |
|
||||||
|
| `scripts/regression.js` `assertCodexAppUnroutedNotificationRoutingContract` | 5076-5113 | thread 索引、负缓存、磁盘 adoption。 |
|
||||||
|
| `scripts/regression.js` `main --target` | 6032-6173 | 可增加 `gitea-workflow-codexapp` 独立目标,避免每次跑完整回归。 |
|
||||||
|
| `scripts/regression.js` Codex App 集成段 | 7239-7622 | mock app-server 下的 session、MCP config、collaboration、retry、Goal、runtime warning、工具调用。 |
|
||||||
|
| `scripts/mock-codex-app-server.js` `ensureThread`/`completeMcpToolTurn`/`startTurn`/`handleRequest` | 91-114、670-737、863-1023、1066-1250 | 可扩展为断言 gitea MCP 配置、thread/start/resume 和 turn/completed。 |
|
||||||
|
| `package.json` `regression` script | 8 | 统一执行 `node scripts/regression.js`。 |
|
||||||
|
|
||||||
|
建议新增 mock 行为:在 `completeMcpToolTurn` 输出 gitea server 的 `type/command/args/env`(token 脱敏),在 `thread/start`/`thread/resume` 记录 config fingerprint;新增回归目标仅断言协议形状和状态机,不调用真实 Gitea。
|
||||||
|
|
||||||
|
## 当前验证结果
|
||||||
|
|
||||||
|
- `node scripts/regression.js --target codexapp-retry-runtime`:通过。
|
||||||
|
- `node scripts/regression.js --target codexapp-unrouted-routing`:通过。
|
||||||
|
- `node scripts/regression.js --target codexapp-stale-running`:通过。
|
||||||
|
- 本任务没有修改 `server.js`、`lib/` 或 `public/`;仅新增本分析文件(以及临时计划 CSV,完成后删除)。校验时工作区已存在其他并行任务对 `server.js`/`lib` 的改动,本任务未触碰、未回退这些改动。
|
||||||
134
.planning/2026-08-23-gitea-workflow/plan-review.md
Normal file
134
.planning/2026-08-23-gitea-workflow/plan-review.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Gitea Workflow 实施计划审查
|
||||||
|
|
||||||
|
审查日期:2026-08-24
|
||||||
|
审查范围:`task_plan.md`、`findings.md`、`Gitea Workflow TO DO list.csv` 及用户目标“完整实现 Gitea Webhook → Codex App 持久会话 → 官方 gitea-mcp → Gitea 回执的可靠工作流,并提供管理页面、队列、恢复、测试和文档”。本审查只产生本文件,不修改业务代码或其它计划文件。
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
当前计划在“阶段主题”层面与 CSV 基本一致,但还不能作为可直接执行和可验收的实施基线。建议先补齐 Phase 1 的协议、状态机、数据模型和安全设计,再进入实现;否则最容易出现“功能链路能跑一次,但重启、重复 Webhook、回执失败或并发时不可靠”的假完成。
|
||||||
|
|
||||||
|
建议结论:**有条件通过,暂不进入核心编码**。以下 P0 项未关闭前,不应把 CSV 中的核心实现项标记为 DONE。
|
||||||
|
|
||||||
|
## 计划与 CSV 一致性
|
||||||
|
|
||||||
|
| 计划范围 | 对应 CSV | 一致性 | 审查意见 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Phase 1 需求、协议、设计(`task_plan.md:13-18`) | 1、2 | 基本一致 | CSV 第 1 项为 `IN_PROGRESS`,与计划“进行中”一致;但“已完成边界汇总”和“设计文档尚在编写”的完成定义没有拆开。 |
|
||||||
|
| Phase 2 状态、Webhook、工作区(`task_plan.md:20-25`) | 3、4 | 基本一致 | 计划把队列/恢复和工作区放在同一阶段,CSV 分成两项;缺少明确前置依赖、交付物和负责人。 |
|
||||||
|
| Phase 3 Codex App、MCP、回执(`task_plan.md:27-32`) | 5 | 基本一致 | CSV 将线程注入、监听、补触发、REST 兜底合成一项,无法分别验收可靠性。 |
|
||||||
|
| Phase 4 管理界面、测试(`task_plan.md:34-39`) | 6、7 | 基本一致 | 计划中的重启恢复、并发、自触发回归没有在 CSV 中单列,容易被“测试已补充”掩盖。 |
|
||||||
|
| Phase 5 整合、验证、交付(`task_plan.md:41-45`) | 8、9、10 | 一致 | 但“解决冲突”“协议形状检查”“文档/审计”均没有明确输出文件、命令和通过阈值。 |
|
||||||
|
|
||||||
|
总体上,CSV 的 10 项与计划的 15 个子项不是一一映射关系。建议在计划和 CSV 中增加稳定的任务 ID(例如 P1-01、P2-02),并为每项写明 `交付物 / 依赖 / 验收命令或证据 / 负责人`;否则状态同步只能凭文字判断。
|
||||||
|
|
||||||
|
## P0 阻断项
|
||||||
|
|
||||||
|
### P0-1:Phase 1 没有可执行的退出条件
|
||||||
|
|
||||||
|
计划只写“核验协议”“完成 PRD、状态机、数据模型和安全设计”(`task_plan.md:16-17`),没有指定文档路径、协议版本、决策结果或评审证据。`findings.md` 目前只有研究结论,没有状态转移表、字段定义、接口契约或威胁模型。
|
||||||
|
|
||||||
|
进入 Phase 2 前至少应产出并评审:
|
||||||
|
|
||||||
|
- Webhook 请求/响应和签名头的版本化协议矩阵;官方 `gitea-mcp` 版本、启动参数、stdio 生命周期和工具能力清单。
|
||||||
|
- 任务/会话/回执状态机(状态、事件、合法转移、重试上限、超时、取消、死信、人工恢复)。
|
||||||
|
- 持久化数据模型、唯一键、索引、迁移/版本策略和崩溃恢复规则。
|
||||||
|
- 线程、任务、Webhook delivery、评论和回执之间的关联契约。
|
||||||
|
- 安全威胁模型与密钥生命周期设计。
|
||||||
|
|
||||||
|
### P0-2:持久化方案无法证明并发安全和崩溃一致性
|
||||||
|
|
||||||
|
`findings.md:17` 仅指出现有 JSON 持久化或“新增独立存储模块”,但没有做出选择。队列、去重、状态转移、会话映射和回执确认若继续散落写 JSON,会遇到并发覆盖、半写文件、重复消费和重启丢失;这与目标中的“持久化、可靠、可恢复”直接冲突。
|
||||||
|
|
||||||
|
计划必须明确单一事实源及原子性边界:至少要定义 inbox(Webhook 去重)、任务状态、会话映射、outbox(评论/补发意图)如何在一次事务或等价原子操作中落盘;并定义锁、文件替换、fsync、备份/恢复和 schema 迁移。若采用 JSON,需证明单写者/进程锁和崩溃恢复;若不能证明,应在计划中选择具备事务语义的存储。
|
||||||
|
|
||||||
|
### P0-3:状态机、幂等和回执可靠性没有闭环
|
||||||
|
|
||||||
|
`findings.md:34-39` 给出了“先验签/去重/过滤”“最多一次隐藏补触发”“REST 兜底”的原则,但没有定义可判定的幂等键、评论标记、确认窗口、重复运行保护或失败终态。尤其“任务完成但没有回帖”时,如何区分 MCP 调用已成功但查询延迟、调用超时、网络重试导致重复评论,当前不可执行。
|
||||||
|
|
||||||
|
需要补充:
|
||||||
|
|
||||||
|
- delivery、任务、turn、评论发送各自的唯一 ID 及跨表关联。
|
||||||
|
- 评论内容中的稳定 run/attempt 标记或等价查询条件,保证 MCP、补触发和 REST 兜底不会重复发同一回执。
|
||||||
|
- Gitea 查询的最终一致性轮询窗口、退避、最大次数和“未知”状态;未知不能直接当失败重跑。
|
||||||
|
- 重试分类(网络/5xx、4xx 权限、超时、进程崩溃)、退避与死信/人工重放策略。
|
||||||
|
- 重启时每个状态的恢复动作,特别是 `running`、`waiting_user`、`receipt_pending` 和 `cancel_requested`。
|
||||||
|
|
||||||
|
### P0-4:三方协议的真实形状和版本未锁定
|
||||||
|
|
||||||
|
`findings.md:19-25` 只记录官方仓库和大致参数,`task_plan.md:29-30` 也没有 app-server 方法、事件类型或 MCP 注入字段。没有固定版本和 mock 契约,无法验收“线程级注入”和“turn 状态监听”是否真的走原生协议。
|
||||||
|
|
||||||
|
应在 Phase 1 固化并纳入 mock/回归断言:
|
||||||
|
|
||||||
|
- Gitea Webhook 事件类型、评论字段路径、签名算法/头、delivery ID、超时和 HTTP 返回语义。
|
||||||
|
- Codex App `initialize/initialized`、能力探测、`thread/start`、线程恢复、turn 事件和取消/中止的确切参数形状。
|
||||||
|
- `thread/start.config.mcp_servers.*` 的线程级 env 注入、stdio 子进程启动/退出、超时和错误事件;不得退回动态工具或进程级来源上下文。
|
||||||
|
- 官方 `gitea-mcp` 版本/校验和、`GITEA_HOST` 与 token 注入方式、工具列表和权限边界。
|
||||||
|
|
||||||
|
### P0-5:全写权限下的安全边界不足
|
||||||
|
|
||||||
|
计划确认“开放官方 gitea-mcp 全部写权限”(`task_plan.md:63`),但安全设计只写 HMAC 和 Token 分离(`task_plan.md:62`)。这不足以支撑公网 Webhook 和自动接入。至少要覆盖重放、密钥轮换、日志脱敏、仓库/主机 allowlist、请求体大小和限流、路径穿越、命令注入、工作区权限、MCP 子进程隔离,以及管理页面的认证/授权/CSRF。
|
||||||
|
|
||||||
|
同时要明确 Bot 自评论过滤不能只依赖用户名:应定义稳定的 bot identity、来源 delivery/run 标记和异常情况下的 fail-closed 行为,避免自触发风暴(`findings.md:34-36`)。
|
||||||
|
|
||||||
|
### P0-6:跨模块写冲突和整合策略未规划
|
||||||
|
|
||||||
|
Phase 5 写“整合子任务改动并解决冲突”(`task_plan.md:43`),但没有任务分区、文件所有权、接口冻结点或整合顺序。预期热点很可能包括 `server.js`、配置/持久化模块、Codex App turn 路径、MCP 配置、路由和前端状态;多个实现项若直接改同一入口,会产生不可审计的隐式冲突。
|
||||||
|
|
||||||
|
进入实现前应把工作拆成不重叠边界(例如 domain/state、webhook/queue、workspace、app-server adapter、API/UI、tests/docs),先冻结共享接口,再按依赖顺序合并;禁止以“最后人工解决冲突”作为验收条件。
|
||||||
|
|
||||||
|
## P1 重要遗漏与可执行性问题
|
||||||
|
|
||||||
|
### 1. 队列与恢复
|
||||||
|
|
||||||
|
Phase 2 只列“状态、队列、去重和恢复”(`task_plan.md:22`),缺少全局并发上限为 2 的调度算法、公平性、每仓库串行锁的租约/心跳、进程崩溃后的 stale runner 判定、优雅停机、取消排队与中止 turn 的区别、超时、死信和人工重放。应为每一类恢复场景写出输入、状态变化和预期结果,并加入 kill/restart 测试。
|
||||||
|
|
||||||
|
### 2. Webhook 与自动接入
|
||||||
|
|
||||||
|
`findings.md:5-7` 未定义配置校验、首次接入的授权边界、仓库 URL/owner/repo 的规范化、默认分支和私有仓库权限失败处理。需要明确先快速返回 HTTP,再异步入队还是同步处理;验签失败、重复 delivery、非评论事件、非目标评论、Bot 自评论分别返回什么,避免 Gitea 重试风暴。
|
||||||
|
|
||||||
|
### 3. 工作区与 Git 认证
|
||||||
|
|
||||||
|
`findings.md:40` 的“脏工作区不 reset、不覆盖”是原则,不是行为契约。要决定脏状态时任务是失败、暂停、继续当前会话还是创建隔离 worktree;定义分支/ref 切换、fetch 冲突、仓库删除、磁盘不足、凭据泄露(命令行/日志/remote config)和清理策略。持久 checkout 与“同仓库串行”还需明确锁的持有范围。
|
||||||
|
|
||||||
|
### 4. Codex App 会话与 waiting_user
|
||||||
|
|
||||||
|
计划确认会话键(`task_plan.md:59`)和固定 `codexapp + yolo`(`task_plan.md:58`),但没有说明首次建线程、重启后的线程恢复、线程不存在/过期、app-server 断线、turn 超时及取消后的映射。`waiting_user`(`task_plan.md:31`、`task_plan.md:64`)没有定义由哪类 Gitea 评论恢复、是否阻塞同仓库、超时/过期和最大连续轮数;“连续评论投递”也没有顺序和幂等规则。
|
||||||
|
|
||||||
|
### 5. 管理 API 与前端
|
||||||
|
|
||||||
|
Phase 4 只写“实现管理 API 和前端页面”(`task_plan.md:36`),没有端点契约、权限角色、敏感配置展示规则、暂停/停用/取消/中止的并发控制、审计记录格式、分页过滤、实时刷新和错误提示。管理页面不能成为绕过队列状态机的第二套写入口;所有操作应复用同一领域命令和审计链。
|
||||||
|
|
||||||
|
### 6. 测试与验收证据
|
||||||
|
|
||||||
|
CSV 第 7、9 项虽覆盖“测试、构建、协议验收”,但没有测试矩阵、通过阈值或运行命令。至少应覆盖:重复/乱序 Webhook、伪造/重放签名、Bot 自触发、首次接入、同仓库并发、不同仓库并发上限、脏工作区、clone/fetch 失败、app-server 断线、MCP 缺失/工具报错、turn 中止、waiting_user、MCP 成功但查询延迟、补触发成功/失败、REST 兜底、进程 kill 后恢复、密钥轮换和管理 API 未授权。
|
||||||
|
|
||||||
|
协议测试不能只断言“调用成功”,还要断言真实参数形状、线程级 env、没有重复顶层字段、调用的是 MCP 工具而非动态工具,并保存 mock 输入/输出作为审计证据。应明确单元/集成/E2E 的最小通过标准和 CI/本地命令。
|
||||||
|
|
||||||
|
### 7. 运维、可观测性与交付
|
||||||
|
|
||||||
|
用户目标包含队列、恢复和文档,但计划未列指标、结构化日志、关联 ID、告警、数据保留、备份恢复、升级/回滚、配置校验和运行手册。Phase 5 的“文档、审计和最终交付”(`task_plan.md:45`)应明确至少包括架构/状态机、部署配置、Webhook 配置、密钥轮换、故障处理、恢复/重放、权限说明和已知限制。
|
||||||
|
|
||||||
|
## 建议的执行顺序与退出条件
|
||||||
|
|
||||||
|
1. **先完成 Phase 1 冻结契约**:补齐上述协议矩阵、状态机、schema/迁移、领域命令、错误/重试表、安全威胁模型和 mock fixture;每项有文档路径和评审人。
|
||||||
|
2. **再实现持久化领域核心**:先落地 inbox、任务、会话、outbox、审计的原子状态转移和恢复算法,再接 Webhook/工作区,避免入口先写出不可恢复的副作用。
|
||||||
|
3. **接入 Codex App 与 gitea-mcp**:以固定版本和协议 mock 验证线程级配置、turn 事件、取消/重连;所有回执发送经过幂等 outbox。
|
||||||
|
4. **最后接 API/UI 和故障测试**:API/UI 只发领域命令;并发、重启、自触发和回执异常测试必须在标记 Phase 4 完成前通过。
|
||||||
|
5. **Phase 5 交付门槛**:构建、静态检查、全测试和协议形状断言均有可复现命令;文档、审计样例、恢复演练记录和回滚方案齐全。
|
||||||
|
|
||||||
|
推荐把每个阶段的状态改成“未开始/进行中/阻塞/完成”,并在完成条件中引用证据文件或测试名称,而不是只保留勾选框。
|
||||||
|
|
||||||
|
## 最小验收清单(对应用户目标)
|
||||||
|
|
||||||
|
- Webhook:合法评论可入队;伪造、重放、重复 delivery、非目标事件和 Bot 自评论均按契约处理。
|
||||||
|
- 持久会话:会话键稳定;重启后任务、锁、turn 和 waiting_user 状态可恢复且不重复执行。
|
||||||
|
- 官方 MCP:固定版本、线程级配置、凭据不落盘/不泄露;读写工具调用可被 mock 和审计验证。
|
||||||
|
- 回执:成功评论可确认;未知状态不会盲目重跑;只允许一次补触发,之后 REST 兜底仍幂等。
|
||||||
|
- 队列/并发:同仓库串行、不同仓库并行、全局上限 2 可证明;暂停、取消、终止和死信可操作。
|
||||||
|
- 管理面:认证授权、审计、状态查询和运维操作与领域状态机一致,不暴露 token/secret。
|
||||||
|
- 可靠性测试:覆盖崩溃恢复、并发、网络/权限/协议失败和自触发回归,并有可重复命令与结果。
|
||||||
|
- 文档交付:架构、配置、部署、密钥、故障恢复、重放、回滚和已知限制齐全。
|
||||||
|
|
||||||
|
## 审查结论摘要
|
||||||
|
|
||||||
|
CSV 与计划没有明显的宏观漏项,但粒度、状态和验收证据不一致;当前最大的风险不是缺少页面或接口,而是持久化原子性、状态机幂等、三方协议形状和全写权限安全边界尚未冻结。补齐 P0 并将其转成带 ID/依赖/证据的任务后,计划才具备可靠实施条件。
|
||||||
97
.planning/2026-08-23-gitea-workflow/progress.md
Normal file
97
.planning/2026-08-23-gitea-workflow/progress.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Gitea Workflow 进度
|
||||||
|
|
||||||
|
## 2026-08-23
|
||||||
|
|
||||||
|
### 当前阶段:需求、协议与架构文档化
|
||||||
|
|
||||||
|
- 已完成用户需求 grilling 对齐。
|
||||||
|
- 已确认 cc-web 本地会话、Codex App、MCP 注入和 turn 完成代码入口。
|
||||||
|
- 已确认官方 gitea-mcp 的 stdio、环境变量和工具覆盖。
|
||||||
|
- 已创建主实施计划,准备并行派发文档与协议核验子任务。
|
||||||
|
|
||||||
|
### 文件
|
||||||
|
|
||||||
|
- `.planning/2026-08-23-gitea-workflow/task_plan.md`
|
||||||
|
- `.planning/2026-08-23-gitea-workflow/findings.md`
|
||||||
|
- `.planning/2026-08-23-gitea-workflow/progress.md`
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
| 测试 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 当前 git status | 工作区初始干净 |
|
||||||
|
| codebase-memory 索引 | ready |
|
||||||
|
|
||||||
|
## 2026-08-24
|
||||||
|
|
||||||
|
### 当前阶段:集成验收与交付
|
||||||
|
|
||||||
|
- 已落地 `lib/gitea-workflow-domain.js`、`store`、`queue`、`service`,统一由 `server.js` 挂接。
|
||||||
|
- 已落地 Webhook HMAC/mention/delivery 去重、HTTPS 工作区 clone/fetch、临时 Git 凭据和脏目录阻塞。
|
||||||
|
- 已落地 Codex App 线程级官方 `gitea-mcp`、turn 标记、回执查询、一次只补发回执和 REST 兜底。
|
||||||
|
- 已落地配置、仓库、任务、审计、暂停/恢复/取消/中止管理 API 与前端页面。
|
||||||
|
- 专项测试全部通过:Webhook 回归 9 场景、核心 7 项、工作区、Codex、管理页面。
|
||||||
|
- `npm run regression` 已在 60 秒门限内完成并输出 `Regression checks passed.`;此前的长时观察记录已由最终回归结果覆盖。另已通过 `codexapp-unrouted-routing`、`composer-slash-routing`、`codexapp-stale-running` 目标回归。
|
||||||
|
- 当前主机未安装 `gitea-mcp` 可执行文件,也未配置真实 Gitea host/token;真实出站 MCP/REST 回帖仍需部署后 smoke test。
|
||||||
|
|
||||||
|
### 2026-08-24 主任务终审补强
|
||||||
|
|
||||||
|
- 修正隐藏回执补发轮次的 turn 标识校验:允许初始预分配标识与历史补发标识集合,避免
|
||||||
|
Agent 已回帖但因 Codex 实际 turnId 变化被误判为缺失。
|
||||||
|
- 查询 Gitea 回帖状态为 `unknown` 时改为保守失败:不自动补发/REST 覆盖,先发送状态
|
||||||
|
告警并标记 `failed_reply`,与 CODEX-INTEGRATION、PRD、ARCHITECTURE 统一。
|
||||||
|
- 工作区成功/脏目录状态回写 Workflow Store;管理页补齐仓库工作区、队列数、活动任务、
|
||||||
|
脏目录原因和审计日志镜像;配置接口校验 Gitea 地址并保留 0600 凭据文件。
|
||||||
|
- Gitea 管理 JS/CSS 使用独立缓存版本,避免只修改管理资源时命中旧缓存。
|
||||||
|
- 修正管理停用仓库后的规范化 Webhook 路径:`enqueueNormalizedTask` 现在返回明确
|
||||||
|
`ok`,对已停用仓库拒绝入队并写入审计,避免通过独立 receiver 绕过停用控制。
|
||||||
|
- 管理页保存的并发上限现会在重启后被 Workflow 服务读取;环境变量凭据只同步“已配置”
|
||||||
|
标志,不把明文 Secret 写入管理状态。
|
||||||
|
- 工作区脏目录现在记录 `dirtySessionKey`:同一 Issue/PR 会话后续 turn 可安全沿用现场,
|
||||||
|
跳过 fetch/checkout;其他讨论仍进入 `blocked_workspace`,且每轮结束会重新探测并在清洁后
|
||||||
|
自动清除脏标记。
|
||||||
|
- Webhook 对完整 `ccweb-gitea` 隐藏标识做前置过滤,即使 Gitea 事件缺少作者字段也不会
|
||||||
|
触发自循环。
|
||||||
|
- 队列的工作区阻塞、失败、取消和中止终态现在统一发送 Gitea 状态评论;REST 兜底成功
|
||||||
|
会单独标记“REST 兜底回帖”,避免用户只看到初始排队消息。
|
||||||
|
- 管理配置更新和控制操作均强制要求 `reason`,并在接口层返回 `reason_required`,保证审计
|
||||||
|
记录不会出现无原因的人工运维动作。
|
||||||
|
|
||||||
|
### 终审验证
|
||||||
|
|
||||||
|
| 测试 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| Gitea 专项 5 脚本 | 全部通过 |
|
||||||
|
| `node --check` + `git diff --check` | 通过 |
|
||||||
|
| `timeout 60s npm run regression` | `Regression checks passed.` |
|
||||||
|
| 隔离服务启动/HTTP 冒烟 | 根页面 200;未认证管理 API 401;未配置 Secret 的 Webhook 503 |
|
||||||
|
|
||||||
|
### 主任务验收补强
|
||||||
|
|
||||||
|
- 复核子任务实际代码、管理页面/API 挂接、文档和持久化边界;未发现重复路由或
|
||||||
|
覆盖核心模块的问题。
|
||||||
|
- 修正 `server.js` 启动时环境配置同步:只有实际提供的环境字段才覆盖管理镜像,
|
||||||
|
避免空的 `CC_WEB_GITEA_WORKSPACE_ROOT` 或并发字段覆盖页面已保存配置。
|
||||||
|
- 重新通过五个 Gitea 专项脚本、`node --check`、`git diff --check` 和
|
||||||
|
`timeout 60s npm run regression`。
|
||||||
|
- 隔离实例 HTTP 冒烟再次通过:根页面 200、未认证管理 API 401、未配置 Secret
|
||||||
|
的 Webhook 503;未触碰当前 PM2 进程。
|
||||||
|
- 追加运行时收敛保护:初始/补发 Codex turn 启动被拒绝时立即清理 waiter 并进入
|
||||||
|
回帖失败/REST 兜底路径;任务持久化 `turnState/turnStartedAt/turnUpdatedAt`,
|
||||||
|
管理页可看到更准确的 Turn 状态。
|
||||||
|
- 工作区校验新增 Gitea host 白名单,阻止 Webhook 携带跨实例 clone URL;队列恢复
|
||||||
|
和入队按评论创建时间(缺失时按接收/创建时间)排序,满足同讨论串顺序处理。
|
||||||
|
- 上述补强后核心、Webhook、管理专项测试与语法/差异检查均再次通过。
|
||||||
|
- 启动配置镜像现在会读取 Secret 文件实际内容后再标记“已配置”,不存在或空文件
|
||||||
|
不会造成管理页虚报凭据可用;隔离 HTTP 冒烟仍通过。
|
||||||
|
- 对“每个 @ 必须有回执”再补强:停用仓库或 Webhook 异步入队异常会发送带任务标识的
|
||||||
|
Gitea 状态回帖,而不是只写本地日志。
|
||||||
|
- 澄清交互采用独立 `waiting_user` 隐藏回执标识;Agent 提问不会被误判为最终回帖
|
||||||
|
缺失,也不会触发重复修改型 reply retry。未配置 Bot Token 时 Webhook 直接 503,
|
||||||
|
防止接受无法回帖的任务。
|
||||||
|
- 最终专项脚本、语法检查、差异检查和完整 regression 均通过。
|
||||||
|
- 复核“文档与集成测试”子任务回传:7 个 fixture 均可解析,独立脚本未加载
|
||||||
|
`server.js`,9/9 场景覆盖验签、过滤、恢复、回帖补偿、MCP 和安全边界;未发现
|
||||||
|
该子任务修改 `server.js` 的证据。
|
||||||
|
- 主任务随后补上状态评论持久化幂等键(`taskId:status`),使 ARCHITECTURE/PRD
|
||||||
|
中的评论幂等契约与运行实现一致;专项测试继续通过。
|
||||||
340
.planning/2026-08-23-gitea-workflow/protocol-research.md
Normal file
340
.planning/2026-08-23-gitea-workflow/protocol-research.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# Gitea Webhook 与官方 gitea-mcp 协议研究
|
||||||
|
|
||||||
|
> 研究性质:只读核验;未修改 `server.js`、`lib/`、`public/`。
|
||||||
|
>
|
||||||
|
> 核验时间:2026-08-24(Asia/Shanghai)。
|
||||||
|
>
|
||||||
|
> 版本口径:Gitea 文档采用 versioned 1.24 文档;Gitea 源码采用 GitHub
|
||||||
|
> `go-gitea/gitea` 提交 `4852091e851060d2233fcbf789727b1c5dba4bfc`
|
||||||
|
> (2026-08-23);`gitea-mcp` 当前 `main` 为
|
||||||
|
> `815a0e26aab26c2df0963f7fca172a6f5b4a2e41`(2026-08-23),最新正式发行版
|
||||||
|
> 为 `v1.6.0`,其提交为 `290d06b40b9fd225e53ec9fa4be1431dc787d0ff`
|
||||||
|
> (2026-07-30)。当前 `main` 已切换到官方 MCP Go SDK,和 v1.6.0 的依赖/实现
|
||||||
|
> 不完全相同;部署时应固定一个已验证的二进制或提交,不要隐式跟随 `main`。
|
||||||
|
|
||||||
|
## 1. 官方来源
|
||||||
|
|
||||||
|
### Gitea
|
||||||
|
|
||||||
|
- Webhook 文档(1.24):
|
||||||
|
<https://gitea.com/gitea/docs/raw/branch/main/versioned_docs/version-1.24/usage/webhooks.md>
|
||||||
|
(渲染页:<https://docs.gitea.com/1.24/usage/webhooks/>)。
|
||||||
|
- API 使用说明(1.24):
|
||||||
|
<https://gitea.com/gitea/docs/raw/branch/main/versioned_docs/version-1.24/development/api-usage.md>。
|
||||||
|
- API 1.24.7 “Add a comment to an issue”:
|
||||||
|
<https://docs.gitea.com/api/1.24/operations/issue-create-comment/>。
|
||||||
|
- Webhook 发送逻辑:
|
||||||
|
<https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/services/webhook/deliver.go>
|
||||||
|
- Webhook payload 定义:
|
||||||
|
<https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/modules/structs/hook.go>
|
||||||
|
- Issue/PR/Comment 结构:
|
||||||
|
<https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/modules/structs/issue.go>、
|
||||||
|
<https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/modules/structs/pull.go>、
|
||||||
|
<https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/modules/structs/issue_comment.go>。
|
||||||
|
- 官方集成测试(普通 Issue/PR 评论):
|
||||||
|
<https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/tests/integration/repo_webhook_test.go>
|
||||||
|
(`Test_WebhookIssueComment`、`Test_WebhookPullRequestComment`)。
|
||||||
|
|
||||||
|
### gitea-mcp
|
||||||
|
|
||||||
|
- 仓库:<https://gitea.com/gitea/gitea-mcp>。
|
||||||
|
- 当前 README(启动、工具表、HTTP 说明):
|
||||||
|
<https://gitea.com/gitea/gitea-mcp/src/branch/main/README.md>。
|
||||||
|
- 当前启动参数与环境变量源码:
|
||||||
|
<https://gitea.com/gitea/gitea-mcp/src/branch/main/cmd/cmd.go>。
|
||||||
|
- 当前 MCP/HTTP 运行与限制:
|
||||||
|
<https://gitea.com/gitea/gitea-mcp/src/branch/main/operation/operation.go>。
|
||||||
|
- 当前 Gitea API 客户端、超时、重定向和认证:
|
||||||
|
<https://gitea.com/gitea/gitea-mcp/src/branch/main/pkg/gitea/gitea.go>、
|
||||||
|
<https://gitea.com/gitea/gitea-mcp/src/branch/main/pkg/gitea/rest.go>。
|
||||||
|
- 最新发行版:<https://gitea.com/gitea/gitea-mcp/releases/tag/v1.6.0>。
|
||||||
|
|
||||||
|
## 2. Gitea Webhook 协议
|
||||||
|
|
||||||
|
### 2.1 请求、事件头和签名
|
||||||
|
|
||||||
|
Gitea Webhook 通常向目标 URL 发送 JSON `POST`。官方文档的示例明确要求
|
||||||
|
`POST`、`Content-Type: application/json`,并给出以下头:
|
||||||
|
|
||||||
|
```text
|
||||||
|
X-Gitea-Delivery: <UUID>
|
||||||
|
X-Gitea-Event: <event>
|
||||||
|
X-Gitea-Signature: <hex SHA-256 HMAC>
|
||||||
|
```
|
||||||
|
|
||||||
|
当前 Gitea 源码 `services/webhook/deliver.go` 还发送:
|
||||||
|
|
||||||
|
```text
|
||||||
|
X-Gitea-Event-Type: <internal event type>
|
||||||
|
X-Gitea-Hook-Installation-Target-Type: <system|repository|organization|user|default>
|
||||||
|
X-Gogs-* # 兼容 Gogs
|
||||||
|
X-Hub-Signature: sha1=<hex>
|
||||||
|
X-Hub-Signature-256: sha256=<hex>
|
||||||
|
X-GitHub-* # 兼容 GitHub
|
||||||
|
```
|
||||||
|
|
||||||
|
事件名有两个层次,不能混淆:
|
||||||
|
|
||||||
|
- Issue 普通评论:`X-Gitea-Event: issue_comment`,内部类型也是
|
||||||
|
`issue_comment`。
|
||||||
|
- PR 普通评论:`X-Gitea-Event: issue_comment`,但当前源码的
|
||||||
|
`X-Gitea-Event-Type` 为 `pull_request_comment`。
|
||||||
|
|
||||||
|
因此接收端应优先使用 `X-Gitea-Event` 判断大类,再结合 payload 的
|
||||||
|
`is_pull`/`pull_request` 或 `X-Gitea-Event-Type` 区分 Issue 与 PR;不要只依赖
|
||||||
|
某个新版本才出现的扩展头。
|
||||||
|
|
||||||
|
配置 Secret 后,当前发送端对**原始请求 body 字节**计算:
|
||||||
|
|
||||||
|
```text
|
||||||
|
hex(HMAC-SHA256(secret, body))
|
||||||
|
```
|
||||||
|
|
||||||
|
`X-Gitea-Signature` 是不带 `sha256=` 前缀的小写十六进制串;源码同时生成
|
||||||
|
GitHub 兼容的 `sha256=<hex>` 形式。接收端必须在 JSON 解析或重新格式化之前读取
|
||||||
|
原始 body,并使用常量时间比较(如 Node `timingSafeEqual`)。文档中的 PHP 示例
|
||||||
|
先 `trim` 后计算,只是示例代码;对本项目应以实际收到的 body 字节为准,不能依赖
|
||||||
|
JSON 等价性或 `trim` 后再验签。
|
||||||
|
|
||||||
|
Webhook payload 中历史 `secret` 字段自 Gitea 1.13 起已弃用,文档标注将移除;
|
||||||
|
不能把 payload 的 `secret` 当作认证依据。应使用独立配置的 HMAC Secret。
|
||||||
|
|
||||||
|
**实现约束:** Webhook 路由应限制 body 大小和 `application/json`,先验签、再解析,
|
||||||
|
验签失败返回 4xx;合法请求在持久化 delivery 与任务后尽快返回 2xx,不能把整个
|
||||||
|
Codex turn 放在 HTTP 请求内同步执行。
|
||||||
|
|
||||||
|
### 2.2 Delivery UUID 与去重
|
||||||
|
|
||||||
|
`X-Gitea-Delivery` 是 Gitea 为每次发送生成的 UUID。官方文档介绍了 Webhook
|
||||||
|
设置页的 Recent Deliveries,但没有规定接收方的去重窗口、重试次数、重试退避、
|
||||||
|
投递顺序或“同一 delivery 是否只会出现一次”。Gitea 源码的 `HookTask.UUID` 在
|
||||||
|
发送端数据库中唯一,但那是 Gitea 自己的出站任务记录,不是对外接收方的幂等协议。
|
||||||
|
|
||||||
|
**本项目必须自行实现:**
|
||||||
|
|
||||||
|
1. 在验签之后、入队之前,以 `(Gitea 实例标识, X-Gitea-Delivery)` 建立持久唯一键。
|
||||||
|
2. 采用原子“插入/已存在”语义;重复请求直接返回成功,不重复 clone、启动 turn 或
|
||||||
|
回帖。
|
||||||
|
3. delivery 记录应包含原始 headers/body 摘要、接收时间、解析结果和最终任务状态,
|
||||||
|
便于审计与恢复。
|
||||||
|
4. 由于官方未承诺顺序,队列按仓库串行化;不要按 delivery UUID 推断先后。
|
||||||
|
|
||||||
|
### 2.3 Issue/PR 普通评论 payload
|
||||||
|
|
||||||
|
当前 Gitea `modules/structs/hook.go` 定义的 `IssueCommentPayload` 为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "created|edited|deleted",
|
||||||
|
"issue": { "number": 123, "title": "...", "body": "...", "user": {}, "repository": {} },
|
||||||
|
"pull_request": { "number": 123, "title": "...", "user": {}, "base": {}, "head": {} },
|
||||||
|
"comment": {
|
||||||
|
"id": 456,
|
||||||
|
"html_url": "...",
|
||||||
|
"pull_request_url": "...",
|
||||||
|
"issue_url": "...",
|
||||||
|
"user": {},
|
||||||
|
"body": "评论正文",
|
||||||
|
"assets": [],
|
||||||
|
"created_at": "...",
|
||||||
|
"updated_at": "..."
|
||||||
|
},
|
||||||
|
"changes": { "body": { "from": "旧正文" } },
|
||||||
|
"repository": { "id": 1, "name": "repo", "full_name": "owner/repo", "clone_url": "..." },
|
||||||
|
"sender": { "id": 2, "login": "user" },
|
||||||
|
"is_pull": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段语义:
|
||||||
|
|
||||||
|
- `action=created` 是新增普通评论;`edited`/`deleted` 是评论编辑/删除。
|
||||||
|
- Issue 评论:`is_pull=false`,通常没有 `pull_request` 对象。
|
||||||
|
- PR 普通评论:`is_pull=true`,当前官方测试确认 `pull_request_comment` 事件的
|
||||||
|
payload 类型仍是 `IssueCommentPayload`,并带 `pull_request` 对象;评论入口仍是
|
||||||
|
PR 所对应的 Issue number。
|
||||||
|
- `comment.body` 是 Markdown 文本;`comment.id` 是回查/审计评论的稳定标识。
|
||||||
|
- `changes.body.from` 只对编辑动作有意义。
|
||||||
|
- `repository.full_name` 是最适合用作工作流仓库键的字段,但仍应做格式校验并拒绝
|
||||||
|
路径穿越/不合法 owner 或 repo。
|
||||||
|
|
||||||
|
**本项目触发过滤建议:** MVP 只消费 `action=created`,并在 `comment.body` 中按
|
||||||
|
明确规则识别 `@ccweb-bot`;编辑/删除不应重新触发任务。还必须在入队前过滤
|
||||||
|
`sender` 为本项目 Bot 的自评论,否则“最终回帖 → Webhook → 新任务”会形成回路。
|
||||||
|
Bot 判断应使用稳定用户 ID/登录名配置,而不是只匹配正文。
|
||||||
|
|
||||||
|
## 3. Gitea REST/API 回执协议
|
||||||
|
|
||||||
|
### 3.1 普通评论写入
|
||||||
|
|
||||||
|
官方 API 1.24 文档的新增评论接口为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments
|
||||||
|
Authorization: token <personal-access-token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"body":"评论正文"}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回的是 Comment 对象。PR 普通评论同样使用该 Issue 评论接口(`index` 为 PR
|
||||||
|
编号);PR review/diff inline comment 是另一套 review API,不能用来代替普通状态
|
||||||
|
回帖。`gitea-mcp` 的 `issue_write` 中 `method=add_comment` 正是这个普通评论路径,
|
||||||
|
而 `pull_request_review_write` 的 `reply_comment` 针对 review comment 线程。
|
||||||
|
|
||||||
|
Gitea API 文档说明支持 `Authorization: token ...`,OAuth token 也接受
|
||||||
|
`Authorization: bearer ...`;URL query token 虽受支持,但本项目禁止把 token 放在
|
||||||
|
URL、日志或评论中。Bot Token 与 Webhook HMAC Secret 必须分离。
|
||||||
|
|
||||||
|
### 3.2 分页、响应上限和错误
|
||||||
|
|
||||||
|
- Gitea 默认 `[api].MAX_RESPONSE_ITEMS=50`;可由实例配置改变。
|
||||||
|
- API 使用 `page` 与 `limit` 分页,并在有更多页时返回 `Link`;还可返回
|
||||||
|
`x-total-count`。
|
||||||
|
- `gitea-mcp` 对外参数多使用 `page`/`per_page`,SDK/API 请求会被 Gitea 的
|
||||||
|
`MAX_RESPONSE_ITEMS` 静默截断;不能把 `per_page` 当作无上限。
|
||||||
|
- Gitea 文档没有给出适用于所有实例的固定速率限制数字。反向代理、云服务或实例
|
||||||
|
配置可能另行限流;实现应对 429/5xx 做有上限的退避,并记录 `Retry-After`,不要
|
||||||
|
假设一定存在该头。
|
||||||
|
- `gitea-mcp` 源码的 REST helper 使用 60 秒 HTTP client timeout;没有看到自动
|
||||||
|
重试逻辑。工具调用失败通常编码为 MCP `tools/call` 结果的 `isError=true`,而
|
||||||
|
malformed request/server fault 才是 JSON-RPC error。Codex App 监听时必须同时处理
|
||||||
|
这两类失败。
|
||||||
|
|
||||||
|
**实现影响:** 正常回执优先由 MCP `issue_write/add_comment` 发送;cc-web 必须通过
|
||||||
|
API 查询或 Webhook 回流确认 comment ID/正文确实出现。若 turn 完成但没有真实回帖,
|
||||||
|
只允许同一会话发起一次“只补发回执、不重复修改”的 turn;仍失败时才用上面的 REST
|
||||||
|
endpoint 兜底,并把 HTTP 状态、响应摘要和是否产生 comment ID 写入审计。
|
||||||
|
|
||||||
|
## 4. 官方 gitea-mcp(stdio)
|
||||||
|
|
||||||
|
### 4.1 启动与配置
|
||||||
|
|
||||||
|
官方 README 给出的 stdio 形状是:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gitea-mcp -t stdio -H https://gitea.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
通过环境传 Token(推荐,避免出现在进程列表):
|
||||||
|
|
||||||
|
```text
|
||||||
|
GITEA_HOST=https://gitea.example.com
|
||||||
|
GITEA_ACCESS_TOKEN=<token>
|
||||||
|
```
|
||||||
|
|
||||||
|
当前 `cmd/cmd.go` 支持的关键参数/环境变量:
|
||||||
|
|
||||||
|
| 用途 | 参数 | 环境变量/默认值 |
|
||||||
|
|---|---|---|
|
||||||
|
| 传输 | `-t`, `--transport` | `MCP_MODE`;默认 `stdio` |
|
||||||
|
| Gitea 地址 | `-H`, `--host` | `GITEA_HOST`;空时 `https://gitea.com` |
|
||||||
|
| Token | `-T`, `--token` | `GITEA_ACCESS_TOKEN`;也支持 `GITEA_ACCESS_TOKEN_FILE` |
|
||||||
|
| 只读 | `-r`, `--read-only` | `GITEA_READONLY=true` |
|
||||||
|
| 工具白名单 | `-O`, `--tools` | `GITEA_TOOLS` |
|
||||||
|
| Scope 白名单 | `-S`, `--scope` | `GITEA_SCOPES` |
|
||||||
|
| 调试/TLS | `-d`, `-k` | `GITEA_DEBUG=true`、`GITEA_INSECURE=true` |
|
||||||
|
| HTTP 备用传输 | `-b`, `-p` | 默认 bind 全接口、port `8080` |
|
||||||
|
| 附件内联上限 | `--max-inline-attachment-bytes` | `GITEA_MAX_INLINE_ATTACHMENT_BYTES`;默认 5 MiB |
|
||||||
|
|
||||||
|
参数优先级以源码为准:显式 token 参数优先于环境;没有显式 token 时读取
|
||||||
|
`GITEA_ACCESS_TOKEN`,再尝试 `GITEA_ACCESS_TOKEN_FILE`。项目应使用固定版本的本地
|
||||||
|
二进制作为 stdio command,并将 `GITEA_HOST`/`GITEA_ACCESS_TOKEN` 放在线程级 MCP
|
||||||
|
进程环境,不要放到全局长驻 app-server 环境或命令行 `-T`。当前 `main` 的 `go.mod`
|
||||||
|
要求 Go 1.26.0/toolchain 1.26.6;因此不建议在本项目运行时用未锁定的
|
||||||
|
`go run ...@latest`(本机旧 Go 或上游变更都会导致不可复现)。
|
||||||
|
|
||||||
|
stdio 模式日志写入 `$HOME/.gitea-mcp/gitea-mcp.log`(目录 0700、滚动保留);源码
|
||||||
|
只在 HTTP 模式把日志复制到 stdout。不要把任何调试输出写入 stdio 的协议 stdout,
|
||||||
|
否则会污染 MCP JSON-RPC 流。
|
||||||
|
|
||||||
|
建议的线程级配置形状(概念示例):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "/opt/cc-web/bin/gitea-mcp-v1.6.0",
|
||||||
|
"args": ["-t", "stdio", "-H", "https://gitea.example.com"],
|
||||||
|
"env": { "GITEA_ACCESS_TOKEN": "<线程专用 Bot Token>" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 工具和写操作
|
||||||
|
|
||||||
|
当前 README 工具表共 54 个工具;未指定 `-r`、`-S`、`-O` 时全部暴露。`-r` 会隐藏
|
||||||
|
所有 Write 工具;`-S` 与 `-O` 是并集筛选。动作型工具通过 `method` 参数再选择
|
||||||
|
具体操作。
|
||||||
|
|
||||||
|
主要只读工具包括:用户/组织(`get_me`、`get_user_orgs`)、搜索(`search_*`)、
|
||||||
|
通知读取、标签/里程碑/wiki/计时/包读取、Issue 列表/读取/附件读取、PR 列表/读取、
|
||||||
|
Actions 配置/运行读取、仓库列表/tree、文件/目录读取、分支/标签/提交/Release 读取、
|
||||||
|
版本查询。
|
||||||
|
|
||||||
|
Write 工具及其源码声明的实际操作如下:
|
||||||
|
|
||||||
|
| 工具 | 写操作 |
|
||||||
|
|---|---|
|
||||||
|
| `notification_write` | `mark_read`、`mark_all_read` |
|
||||||
|
| `label_write` | repo/org label 的 create、edit、delete |
|
||||||
|
| `milestone_write` | create、update/edit、delete |
|
||||||
|
| `wiki_write` | create、update、delete 页面 |
|
||||||
|
| `timetracking_write` | start/stop/delete stopwatch、add/delete time |
|
||||||
|
| `package_write` | 删除 package version(不可逆) |
|
||||||
|
| `issue_write` | create、update、`add_comment`、`edit_comment`、add/remove/replace/clear labels |
|
||||||
|
| `pull_request_write` | create、update、close、reopen、merge、update_branch、add/remove reviewers |
|
||||||
|
| `pull_request_review_write` | create/submit/delete/dismiss review、`reply_comment`、resolve/unresolve thread |
|
||||||
|
| `actions_config_write` | Actions secret/variable 的 upsert/create/update/delete(repo/org) |
|
||||||
|
| `actions_run_write` | dispatch、cancel、rerun workflow run |
|
||||||
|
| `create_repo`/`fork_repo` | 创建/派生仓库 |
|
||||||
|
| `create_or_update_file`/`delete_file` | 创建、更新、删除仓库文件 |
|
||||||
|
| `create_branch`/`delete_branch` | 创建、删除分支 |
|
||||||
|
| `create_tag`/`delete_tag` | 创建、删除 tag |
|
||||||
|
| `create_release`/`delete_release` | 创建、删除 Release |
|
||||||
|
|
||||||
|
这远超“回帖”所需权限。当前产品决策明确要求 MVP 开放官方 gitea-mcp 全部写权限,
|
||||||
|
因此必须同时具备全局暂停、仓库停用、排队取消、turn 中止和完整审计;不能把“工具
|
||||||
|
调用成功”当作安全授权。生产环境仍应为 Bot Token 配置最小可用仓库/组织权限,并在
|
||||||
|
管理面明确显示高风险工具可用。
|
||||||
|
|
||||||
|
### 4.3 HTTP 备用模式的限制(不是本 MVP 主路径)
|
||||||
|
|
||||||
|
当前 `main` 的 HTTP transport 使用 `/mcp`,只接受 POST,启用 Origin 校验,且始终
|
||||||
|
无状态:不使用 `Mcp-Session-Id`、独立 SSE 或 `Last-Event-ID` 恢复。反向代理必须原样
|
||||||
|
转发 `Mcp-Protocol-Version`、`Mcp-Method`、`Mcp-Name`。HTTP 请求可用
|
||||||
|
`Authorization: Bearer <token>` 或 `Authorization: token <token>`,这是逐请求的
|
||||||
|
Gitea credential passthrough,不是 MCP OAuth。HTTP MCP 默认请求体上限由源码提高到
|
||||||
|
32 MiB。若未来不用 stdio 而改 HTTP,必须额外处理这些协议与反代约束。
|
||||||
|
|
||||||
|
## 5. 对 cc-web 工作流的落地影响
|
||||||
|
|
||||||
|
1. **Webhook 接收顺序固定为:** 限制请求 → 读取原始 body → HMAC 验签 → 校验
|
||||||
|
`X-Gitea-Delivery`/事件头 → JSON 解析 → Bot 自评论过滤 → `action=created` 与
|
||||||
|
`@ccweb-bot` 过滤 → 持久化唯一 delivery → 快速 2xx → 入队。
|
||||||
|
2. **会话键:** 使用 `gitea + repository.full_name + issue/PR 类型 + number`;PR
|
||||||
|
普通评论仍使用 `issue_write/add_comment`,不是 review reply。
|
||||||
|
3. **回执确认:** MCP 最终输出不等于 Gitea 已写入;必须查询评论列表/回流 Webhook
|
||||||
|
确认正文和 comment ID。缺失时最多一次隐藏补发 turn,最后 REST 兜底。
|
||||||
|
4. **凭据隔离:** HMAC Secret 只用于入站验签,Bot Token 只用于 MCP/REST/Git;Token
|
||||||
|
通过线程级 env 注入,禁止写入 remote URL、任务正文、日志和 URL query。
|
||||||
|
5. **并发和恢复:** delivery 去重、任务状态、仓库锁和回执审计必须持久化;同仓库
|
||||||
|
串行、不同仓库按全局上限并行。进程重启后 queued 恢复,running 按既定最多一次重试,
|
||||||
|
waiting_user 保留。
|
||||||
|
6. **API 稳定性:** 对 429、5xx、网络超时做有限退避;尊重 Gitea 的分页上限和
|
||||||
|
`MAX_RESPONSE_ITEMS`;MCP 的 `isError=true` 与 JSON-RPC error 均视为失败路径。
|
||||||
|
|
||||||
|
## 6. 不确定项与需集成验收的内容
|
||||||
|
|
||||||
|
- 官方文档没有承诺 Webhook 的重试次数、退避策略、投递顺序或接收端去重语义;需在
|
||||||
|
实际 Gitea 版本通过失败投递/Recent Deliveries 做黑盒验收。
|
||||||
|
- `X-Gitea-Event-Type`、部分 payload 字段和旧版本兼容头可能随 Gitea 版本变化;接收
|
||||||
|
端必须允许缺失扩展头,并以稳定字段/大类事件降级。
|
||||||
|
- API 速率限制取决于 Gitea 版本、实例配置、云服务或反向代理;当前官方资料未提供
|
||||||
|
可直接套用的全局数值。
|
||||||
|
- Token 的精确 scope/仓库权限由目标 Gitea 实例决定;需要用目标 Bot Token 实测
|
||||||
|
`issue_write/add_comment`、读取评论、读取 PR/文件和必要的仓库操作。
|
||||||
|
- 当前 `gitea-mcp` `main` 要求 Go 1.26,最新发行版与 `main` 的 MCP SDK/HTTP 实现
|
||||||
|
不同;必须在 CI/部署机锁定并做 stdio `initialize`、`tools/list`、真实
|
||||||
|
`issue_write` 和错误返回回归。
|
||||||
|
- Gitea Webhook 文档示例 PHP 使用 `trim` 后验签,与当前发送源码“对原始 body 签名”
|
||||||
|
的实现细节存在示例层面的歧义;本项目应以原始字节验签,并用目标实例实际 delivery
|
||||||
|
样本校验。
|
||||||
|
|
||||||
71
.planning/2026-08-23-gitea-workflow/task_plan.md
Normal file
71
.planning/2026-08-23-gitea-workflow/task_plan.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Gitea Workflow 持久化实施计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在 cc-web 内完整落地 Gitea Webhook → 持久 Codex App 会话 → 官方 gitea-mcp → Gitea 回执的工作流,并提供可恢复的队列、工作区和管理界面。
|
||||||
|
|
||||||
|
## 当前阶段
|
||||||
|
|
||||||
|
Phase 5:集成验收与交付
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
### Phase 1:需求与协议确认
|
||||||
|
|
||||||
|
- [x] 汇总用户已确认的产品边界
|
||||||
|
- [x] 核验官方 Gitea Webhook 与 gitea-mcp 协议
|
||||||
|
- [x] 完成 PRD、状态机、数据模型和安全设计
|
||||||
|
- 状态:已完成
|
||||||
|
|
||||||
|
### Phase 2:核心工作流实现
|
||||||
|
|
||||||
|
- [x] 实现配置、状态、队列、去重和恢复
|
||||||
|
- [x] 实现 Webhook 验签、事件过滤和自动接入
|
||||||
|
- [x] 实现 clone/fetch、仓库锁和工作区状态
|
||||||
|
- 状态:已完成
|
||||||
|
|
||||||
|
### Phase 3:Codex App 与回执集成
|
||||||
|
|
||||||
|
- [x] 注入线程级 gitea-mcp 配置
|
||||||
|
- [x] 实现 turn 状态监听、回执确认、补触发和 REST 兜底
|
||||||
|
- [x] 实现 waiting_user 与连续评论投递
|
||||||
|
- 状态:已完成
|
||||||
|
|
||||||
|
### Phase 4:管理界面与测试
|
||||||
|
|
||||||
|
- [x] 实现工作流管理 API 和前端页面
|
||||||
|
- [x] 补充单元、回归、协议 mock 和安全测试
|
||||||
|
- [x] 完成重启恢复、并发和自触发回归
|
||||||
|
- 状态:已完成
|
||||||
|
|
||||||
|
### Phase 5:集成验收与交付
|
||||||
|
|
||||||
|
- [x] 整合子任务改动并解决冲突
|
||||||
|
- [x] 运行测试、构建和真实协议形状检查
|
||||||
|
- [x] 完成文档、审计和最终交付
|
||||||
|
- 状态:已完成
|
||||||
|
|
||||||
|
## 已确认决策
|
||||||
|
|
||||||
|
| 决策 | 结论 |
|
||||||
|
|---|---|
|
||||||
|
| 触发 | Issue/PR 普通评论中的 `@ccweb-bot` |
|
||||||
|
| 实例 | MVP 单个 Gitea 实例,一个全局 Bot |
|
||||||
|
| 未登记仓库 | 首次合法触发自动接入并 clone |
|
||||||
|
| 工作区 | 全局根目录下每仓库持久 checkout |
|
||||||
|
| Git 认证 | HTTPS + Bot Token,临时凭据,不写 remote URL |
|
||||||
|
| 并发 | 同仓库串行,不同仓库并行,全局上限默认 2 |
|
||||||
|
| Agent | 固定 `codexapp` + `yolo` |
|
||||||
|
| 会话键 | `gitea + owner/repo + type + number` |
|
||||||
|
| 交互 | Gitea 主交互,cc-web 镜像和运维 |
|
||||||
|
| 回复 | 状态评论 + MCP 最终回复;缺失时补触发一次,最后 REST 兜底 |
|
||||||
|
| 安全 | HMAC Webhook Secret;Bot Token 与 Secret 分离 |
|
||||||
|
| MCP 权限 | 用户明确要求开放官方 gitea-mcp 全部写权限 |
|
||||||
|
| 恢复 | queued 恢复,waiting_user 保留,running 中断并最多重试一次 |
|
||||||
|
| 运维 | 全局暂停、仓库停用、取消排队、中止 turn、审计日志 |
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---:|---|
|
||||||
|
| 暂无 | 1 | - |
|
||||||
15
.planning/2026-08-24-gitea-codex-turn/findings.md
Normal file
15
.planning/2026-08-24-gitea-codex-turn/findings.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Gitea Codex 工作流实现发现
|
||||||
|
|
||||||
|
- `server.js` 的 `codexAppThreadConfig(session, options)` 最终将每项配置写为 `mcp_servers.<server>`,所以独立适配器应返回与 app-server config 兼容的 `{type:'stdio', command, args, env}`。
|
||||||
|
- `startCodexAppTurn()` 在 `thread/start`/`thread/resume` 后记录 `entry.threadId`,在 `turn/start` 返回后记录 `entry.turnId`;`handleCodexAppTurnComplete()` 是完成监听点。
|
||||||
|
- 运行时事件可能从 `params.turnId`、`params.turn.id`、`params.item.turnId` 取 turnId;关联标记必须同时支持明文状态字段和 HTML 注释隐藏标识。
|
||||||
|
- 官方 gitea-mcp 采用 stdio,启动参数为 `-t stdio -H <host>`,token 通过线程级 env 注入;不应放到长驻 app-server 全局环境。
|
||||||
|
- gitea-mcp 最终评论需要查询确认;查询需匹配资源、Bot 作者和 taskId/turnId/resourceKey 隐藏标识,未知状态不能直接重复执行。
|
||||||
|
- waiting_user 新评论应复用原 `sessionKey/threadId`,但每条评论创建新 task/turn;补触发必须是同一 thread 的隐藏轮次,且最多一次。
|
||||||
|
|
||||||
|
## 实现接口
|
||||||
|
|
||||||
|
- `buildGiteaThreadConfig({cwd, host, accessToken})` 返回 `config['mcp_servers.gitea']`,内部为 `type=stdio`、`gitea-mcp -t stdio -H host` 和线程级 `GITEA_HOST/GITEA_ACCESS_TOKEN`。
|
||||||
|
- `buildWorkflowMarker`/`parseWorkflowMarker` 使用 `<!-- ccweb-gitea v="1" ... -->`,缺少 taskId、turnId 或 resourceKey 时返回 null。
|
||||||
|
- `createGiteaWorkflowCodex` 通过 `verifyReply/listComments/startTurn/sendRestReply` 依赖注入实现外部副作用;`unknown` 查询直接进入保守 `failed_reply`,不会自动补触发。
|
||||||
|
- `server.js` 的 `codexAppThreadConfig` 同时兼容 `{host, accessToken}` 和已构建的 stdio config;`startCodexAppTurn`、`handleCodexAppTurnComplete` 提供 turn 生命周期薄挂接,既有 waiter 在完成时执行回执确认/补偿。
|
||||||
22
.planning/2026-08-24-gitea-codex-turn/progress.md
Normal file
22
.planning/2026-08-24-gitea-codex-turn/progress.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Gitea Codex 工作流实施进度
|
||||||
|
|
||||||
|
## 日志
|
||||||
|
|
||||||
|
- 2026-08-24:读取现有 Gitea Workflow 研究、PRD/ARCHITECTURE 和 Codex App 代码入口;确认本次只改独立模块、最小 server.js 挂接点、协议 mock 与单测。
|
||||||
|
- 2026-08-24:完成代码入口定位;计划审查要求补齐隐藏标识、Bot 身份、回执三态、同线程单次补触发和 REST 终止条件,已纳入验收约束。
|
||||||
|
- 2026-08-24:新增 `lib/gitea-workflow-codex.js`,实现线程级 stdio gitea-mcp 配置、版本化隐藏标记、turnId/threadId 提取、Bot 自评论过滤、waiting_user 连续评论、回执 confirmed/missing/unknown 和单次 reply_retry/REST 兜底。
|
||||||
|
- 2026-08-24:`server.js` 增加独立模块 require、gitea MCP config 合并、turn started/completed 薄挂接,并让既有 Gitea waiter 在 turn 完成时 settle;查询未知状态不再自动重跑。
|
||||||
|
- 2026-08-24:补充 server waiter 对预分配 workflow marker turnId 与实际 Codex turnId 的兼容匹配;首轮旧 marker 可确认,reply_retry 使用实际新 turnId。
|
||||||
|
- 2026-08-24:新增 `scripts/gitea-workflow-codex-unit.js` 协议 mock/单测,覆盖配置形状、隐藏标记、Bot id/login、waiting_user 事件与同线程连续评论、单次补触发、REST 次数和 unknown 保守终态。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
| 命令 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| `node --check`(Codex 适配器、server.js、相关 Gitea 模块) | 通过 |
|
||||||
|
| `node scripts/gitea-workflow-codex-unit.js` | 通过 |
|
||||||
|
| `node scripts/gitea-webhook-regression.js` | 通过,9 场景 |
|
||||||
|
| `node scripts/gitea-webhook-workspace-unit.js` | 通过 |
|
||||||
|
| `node scripts/gitea-workflow-management-unit.js` | 通过 |
|
||||||
|
| `node scripts/gitea-workflow-core-unit.js` | 通过,6 项 |
|
||||||
|
| `timeout 60s npm run regression` | 未通过:既有 MCP 创建会话回归在等待 `background_done` 处超时(scripts/regression.js:6717),未出现本适配器异常;需后续单独修复该既有回归 |
|
||||||
39
.planning/2026-08-24-gitea-codex-turn/task_plan.md
Normal file
39
.planning/2026-08-24-gitea-codex-turn/task_plan.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Gitea Codex 工作流会话与回执实施计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
新增独立的 Codex App/Gitea 工作流适配模块,覆盖线程级官方 gitea-mcp 注入、turnId 关联、waiting_user 连续评论、回执确认、一次补触发和 REST 兜底;以最小挂接点接入 server.js,并用协议 mock 与单测验收。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
- [x] 定位现有 Codex App 线程、turn、MCP 配置和通知路由挂接点
|
||||||
|
- [x] 冻结独立模块 API 与隐藏 turnId/Bot 自评论标记协议
|
||||||
|
- [x] 实现线程级 gitea-mcp 配置、waiting_user 评论串联和回执状态机
|
||||||
|
- [x] 增加 server.js 最小挂接示例与可复制的接入说明
|
||||||
|
- [x] 编写协议 mock 和模块单元测试,覆盖补触发/REST 兜底
|
||||||
|
- [x] 运行语法检查、单测和现有回归并记录结果
|
||||||
|
|
||||||
|
## 完成记录
|
||||||
|
|
||||||
|
- 线程级 `mcp_servers.gitea`、隐藏标识、Bot 过滤、waiting_user 连续评论、一次补发和
|
||||||
|
REST 兜底已接入 `server.js`。
|
||||||
|
- 回帖校验允许历史补发标识集合;查询 `unknown` 时保守失败,不自动重复写操作。
|
||||||
|
- `node scripts/gitea-workflow-codex-unit.js`、Gitea 专项回归和全量 `npm run regression`
|
||||||
|
均通过。
|
||||||
|
|
||||||
|
## 验收约束
|
||||||
|
|
||||||
|
- 隐藏标识采用版本化 HTML 注释,编码后的 payload 必须包含 `taskId`、`turnId` 和 `resourceKey`,解析失败时不得误判为本轮回执。
|
||||||
|
- Bot 自评论优先按稳定 user id 判定;未配置 id 时才回退到大小写无关 login;带有效 ccweb 隐藏标识的评论始终不触发新任务。
|
||||||
|
- 回执确认严格区分 `confirmed`、`missing` 和 `unknown`;查询异常为 `unknown`,禁止据此重复执行代码修改。
|
||||||
|
- 正常回执未确认时最多创建一个 `reply_retry`,且必须复用原 `threadId`;补触发提示词只允许补发回执,不允许修改代码。
|
||||||
|
- 补触发后仍非 `confirmed` 才允许 REST 兜底;REST 成功或最终失败后不得再次补触发。
|
||||||
|
- gitea-mcp 只能出现在 `thread/start.config.mcp_servers.gitea`,host/token 经该线程的 args/env 注入,禁止写入 app-server 进程全局环境。
|
||||||
|
- `server.js` 仅增加 require、配置合并、turn started/completed/user-input 信号等薄挂接;工作流逻辑必须留在独立模块。
|
||||||
|
- 单测必须断言线程配置形状、标记往返/Bot 过滤、waiting_user 同线程投递、查询异常、单次补触发和 REST 兜底次数。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---:|---|
|
||||||
|
| planning-with-files catchup 脚本路径不存在 | 1 | 记录后改为直接读取现有规划文件并继续 |
|
||||||
13
.planning/2026-08-24-gitea-mcp-failure/findings.md
Normal file
13
.planning/2026-08-24-gitea-mcp-failure/findings.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Gitea MCP 失败链路发现
|
||||||
|
|
||||||
|
- 当前宿主机 `command -v gitea-mcp` 无输出;server.js 已有命令存在性检查,
|
||||||
|
缺失时抛出稳定错误码 `gitea_mcp_not_found`。
|
||||||
|
- runner 在命令检查后创建/复用 Codex App 会话,再通过
|
||||||
|
`thread/start.config.mcp_servers.gitea` 注入 stdio MCP;当前没有独立的 MCP
|
||||||
|
initialize 预检。
|
||||||
|
- Codex App `thread/start` 请求超时为 60 秒;如果 MCP 子进程存在但启动后立即
|
||||||
|
退出,任务可能等待较久或只在 app-server 事件出现后才失败。
|
||||||
|
- 队列异常终态会发送一次 `giteabot: 任务失败:...`;正常流程只发送起始的
|
||||||
|
`giteabot: 已收到`,最终正文由 MCP/REST 兜底负责。
|
||||||
|
- 当前工作区存在大量其他代理未提交改动,本轮只改动与 MCP 失败链路直接相关的
|
||||||
|
文件和独立测试/文档,禁止 reset 或覆盖无关变更。
|
||||||
9
.planning/2026-08-24-gitea-mcp-failure/progress.md
Normal file
9
.planning/2026-08-24-gitea-mcp-failure/progress.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Gitea MCP 失败链路进度
|
||||||
|
|
||||||
|
## 记录
|
||||||
|
|
||||||
|
- 2026-08-24:建立本轮持久化计划;确认服务已由 PM2 `ccweb` 在线运行,未执行重启。
|
||||||
|
- 2026-08-24:确认缺失命令已有快速失败,但命令存在且 stdio 握手失败仍缺少预检。
|
||||||
|
- 2026-08-24:计划审查指出必须单独验收失败回执只发送一次、三类故障分流、线程级注入保持不变和不自动安装;已补入计划。
|
||||||
|
- 2026-08-24:新增 `lib/gitea-mcp-probe.js`,在启动 Codex App 前发送 MCP `initialize`,对启动失败、握手错误和超时返回稳定错误码。
|
||||||
|
- 2026-08-24:新增 `fixtures/gitea-workflow/mock-gitea-mcp.js` 与 `scripts/gitea-mcp-probe-unit.js`,预检成功、错误响应、进程退出、超时和命令不存在场景通过。
|
||||||
27
.planning/2026-08-24-gitea-mcp-failure/task_plan.md
Normal file
27
.planning/2026-08-24-gitea-mcp-failure/task_plan.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Gitea MCP 失败链路收尾计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让 Gitea Workflow 在官方 `gitea-mcp` 缺失、无法启动或 stdio 握手失败时,
|
||||||
|
在有限时间内结束任务并发送一次可见失败回执;正常 MCP 链路保持线程级注入,
|
||||||
|
不引入 Docker 依赖、不修改用户未授权的部署配置。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
- [in_progress] 核对当前 Gitea runner、Codex App 启动和回执结算链路
|
||||||
|
- [pending] 增加 gitea-mcp stdio 预检与有限超时错误分类
|
||||||
|
- [pending] 增加针对缺失、启动失败、握手失败的独立回归断言,并验证每类只发一次 giteabot 失败回执
|
||||||
|
- [pending] 同步中文文档和运行配置说明,明确线程级注入与不自动安装约束
|
||||||
|
- [pending] 运行静态检查、单测、Webhook 回归并核对运行状态
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---:|---|
|
||||||
|
| 全局技能路径不存在 | 1 | 改用项目内 `.codex/skills/planning-with-files` 与用户目录 `todo-list-csv` |
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
- 不自动安装外部二进制;安装属于外部系统变更,需用户明确确认。
|
||||||
|
- 预检只验证命令可执行和 MCP initialize 响应,不记录 Token 或完整 stderr。
|
||||||
|
- Gitea Workflow 任务失败回执仍统一使用 `giteabot:` 前缀。
|
||||||
21
.planning/2026-08-24-gitea-webhook-docs/findings.md
Normal file
21
.planning/2026-08-24-gitea-webhook-docs/findings.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 发现记录
|
||||||
|
|
||||||
|
## 现状
|
||||||
|
|
||||||
|
- `docs/gitea-workflow/PRD.md` 与 `ARCHITECTURE.md` 已存在,已约定 MVP 为单
|
||||||
|
Gitea 实例、`ccweb-bot`、同仓库串行、全局并发 2、线程级 `gitea-mcp`。
|
||||||
|
- 当前仓库没有 Gitea 业务实现入口;本轮脚本必须测试协议/状态机纯函数,不应
|
||||||
|
假设或改写 `server.js`。
|
||||||
|
- `package.json` 只有 `npm run regression`;新增脚本应提供独立 `node` 入口,
|
||||||
|
便于实现接入阶段先运行夹具回归。
|
||||||
|
|
||||||
|
## 交付设计
|
||||||
|
|
||||||
|
- `DEPLOYMENT.md`:配置分层、Webhook 反向代理、凭据、目录权限、启动顺序、
|
||||||
|
健康检查、暂停/恢复、备份恢复、故障处置、日志脱敏和容量基线。
|
||||||
|
- `TESTING.md`:测试金字塔、fixtures 协议、场景矩阵、故障注入、恢复/重试
|
||||||
|
断言、MCP `thread/start.config.mcp_servers.gitea` 契约与安全测试。
|
||||||
|
- `scripts/gitea-webhook-regression.js`:零依赖 Node 脚本,加载 fixtures,
|
||||||
|
对原始 body 做 HMAC-SHA256 常量时间校验,验证事件过滤、delivery 去重、
|
||||||
|
队列恢复/并发互斥、回执补发/REST 兜底、线程级 MCP 配置、路径边界以及敏感
|
||||||
|
信息不泄漏。
|
||||||
10
.planning/2026-08-24-gitea-webhook-docs/progress.md
Normal file
10
.planning/2026-08-24-gitea-webhook-docs/progress.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 进度日志
|
||||||
|
|
||||||
|
- 2026-08-24:启用 `todo-list-csv`,建立六步计划和独立清单。
|
||||||
|
- 2026-08-24:确认 PRD/ARCHITECTURE 已由其他工作流提供,本轮仅新增部署/测试
|
||||||
|
文档、独立脚本与 fixtures,避免修改业务主逻辑和现有目标文档。
|
||||||
|
- 2026-08-24:计划审查第一次发现交付范围和测试入口未明确;已修正并复审通过。
|
||||||
|
- 2026-08-24:新增 `DEPLOYMENT.md`、`TESTING.md`、7 个 JSON fixture 文件和独立
|
||||||
|
`scripts/gitea-webhook-regression.js`;脚本覆盖 9 个离线场景。
|
||||||
|
- 2026-08-24:`node --check`、`timeout 60s node scripts/gitea-webhook-regression.js`
|
||||||
|
和 `git diff --check` 全部通过。
|
||||||
30
.planning/2026-08-24-gitea-webhook-docs/task_plan.md
Normal file
30
.planning/2026-08-24-gitea-webhook-docs/task_plan.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Gitea Webhook Agent 文档与回归计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
补齐 Gitea Workflow 的部署运维和测试设计文档,并新增不依赖业务实现的
|
||||||
|
Webhook 协议回归脚本与 fixtures,覆盖验签、事件过滤、队列恢复、回执重试
|
||||||
|
兜底、MCP 线程配置和安全边界;不修改 `server.js` 主逻辑,也不覆盖其他代理
|
||||||
|
正在进行的 Gitea 文档或实现改动。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
1. 盘点并复核 `docs/gitea-workflow/PRD.md`、`ARCHITECTURE.md`,确认需求覆盖和
|
||||||
|
与其他代理的修改边界
|
||||||
|
2. 补齐/校验 PRD 与架构交付基线,并设计部署运维文档固定安全与恢复契约
|
||||||
|
3. 设计测试文档并建立需求到回归场景覆盖矩阵
|
||||||
|
4. 新增独立 webhook 回归脚本与 JSON fixtures,固定直接验收入口
|
||||||
|
5. 运行定向回归、脚本语法检查和差异范围审查
|
||||||
|
6. 完成文档一致性复核并清理临时清单
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 现有 `PRD.md`、`ARCHITECTURE.md` 属于其他代理的工作区:先做只读一致性复核;
|
||||||
|
只有发现本需求明确缺口且确认不覆盖其改动时才做最小追加,否则在新增文档中
|
||||||
|
记录已覆盖项和缺口。
|
||||||
|
- 本轮新增/修改目标为 `docs/gitea-workflow/DEPLOYMENT.md`、`TESTING.md`、
|
||||||
|
`scripts/gitea-webhook-regression.js` 及 `fixtures/gitea-workflow/*.json`,
|
||||||
|
并在新增文档中明确引用 PRD/架构路径。
|
||||||
|
- 独立验收入口固定为:`timeout 60s node scripts/gitea-webhook-regression.js`;
|
||||||
|
脚本输出每个场景的 PASS/FAIL 与最终汇总,不依赖服务启动或网络。
|
||||||
|
- 不修改 `server.js` 主逻辑、`lib/`、`public/`、依赖和其他代理的目标文档。
|
||||||
24
.planning/2026-08-24-gitea-workflow-management/task_plan.md
Normal file
24
.planning/2026-08-24-gitea-workflow-management/task_plan.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Gitea Workflow 管理 API 与页面实施计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在不覆盖 Gitea Workflow 核心领域模块的前提下,为 cc-web 增加可挂接的管理 API、
|
||||||
|
前端管理页面和最小回归测试。页面展示仓库、队列、turn、日志与脏目录原因,并提供
|
||||||
|
全局暂停、仓库启停、取消排队、中止 turn 及操作者/原因审计。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
1. 读取现有前端路由、API 契约并确定挂接边界
|
||||||
|
2. 实现独立 Gitea Workflow 管理 API 适配器与服务注入契约
|
||||||
|
3. 接入 server.js 路由、鉴权与前端资源版本计算
|
||||||
|
4. 实现 Workflow 管理页面脚本、样式并挂到现有入口
|
||||||
|
5. 补充最小 API 与前端契约测试
|
||||||
|
6. 运行语法检查、单测与回归并修复问题
|
||||||
|
7. 汇总变更、清理临时清单并回报交付
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
- 新增管理适配器与前端资源;核心队列/工作区/会话实现由注入的 service 提供。
|
||||||
|
- server.js 只做路由挂接和鉴权,不复制领域状态机。
|
||||||
|
- 兼容 service 不可用时的只读空状态,控制操作返回明确的 503。
|
||||||
|
- 不安装依赖,不修改与本任务无关的现有未提交文件。
|
||||||
57
.planning/2026-08-25-instance-custom-icon/findings.md
Normal file
57
.planning/2026-08-25-instance-custom-icon/findings.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# 实例自定义图标调研记录
|
||||||
|
|
||||||
|
## 已知需求
|
||||||
|
|
||||||
|
- 同一套 cc-web 部署到多台机器时,需要通过不同图标快速区分实例。
|
||||||
|
- 用户希望在设置中自行选择图标。
|
||||||
|
- 自定义结果在拉取最新版后必须保留。
|
||||||
|
|
||||||
|
## 待确认事实
|
||||||
|
|
||||||
|
- 当前默认图标的所有消费点。
|
||||||
|
- 设置页和设置 API 的扩展模式。
|
||||||
|
- 项目现有的 Git 外置运行数据目录与备份方式。
|
||||||
|
- 相关测试框架和浏览器缓存策略。
|
||||||
|
|
||||||
|
## 代码库初步事实
|
||||||
|
|
||||||
|
- 项目为无框架的 Node.js 单体:`server.js` 提供服务,`public/app.js`、`public/index.html`、`public/style.css` 构成主界面。
|
||||||
|
- 默认浏览器/PWA 图标均为 Git 跟踪文件:`public/favicon.ico`、`favicon-32x32.png`、`apple-touch-icon.png`、`icon-192.png`、`icon-512.png`,直接替换它们会在升级时产生冲突或被覆盖,因此不能作为实例自定义存储。
|
||||||
|
- 设置入口位于 `public/index.html`,设置页 HTML 由 `public/app.js` 中的构建函数生成;已有“外观”子页,可复用其导航卡和设置状态样式。
|
||||||
|
- 回归入口为 `npm run regression`,项目还包含多个 `scripts/*-unit.js` 与浏览器集成脚本,适合增加聚焦回归脚本并挂入总回归。
|
||||||
|
- 首轮本地全文检索被仓库中的大文本记录污染;后续必须收窄到 `server.js`、`public/`、`scripts/`,并用精确标识符核验行号。
|
||||||
|
|
||||||
|
## 图标与存储入口
|
||||||
|
|
||||||
|
- `public/index.html` 的浏览器图标链接位于 head,登录页 Logo 使用 `icon-192.png`;`public/app.js` 的通知图标和重新渲染的登录框也直接使用 `/icon-192.png`;`public/sw.js` 的通知默认值同样硬编码该路径。
|
||||||
|
- 这意味着仅改 favicon 不足以形成一致的实例身份,至少应统一浏览器页签、登录 Logo 与通知图标;Web App Manifest 的静态图标是否能动态化需按 PWA 缓存边界保守处理。
|
||||||
|
- 服务端已经支持 `CC_WEB_CONFIG_DIR`,默认指向仓库内 `config/`;测试通常把它指向临时目录。实例图标应落在该目录,生产可用外置目录,默认仓库内目录则通过精确 `.gitignore` 规则保护。
|
||||||
|
- `.gitignore` 已逐项忽略多种 `config/*.json` 运行配置,但尚无实例图标规则;需要在保留用户现有修改的前提下追加精确规则,不能把整个 `config/` 忽略。
|
||||||
|
|
||||||
|
## 选定方案
|
||||||
|
|
||||||
|
- 浏览器端把用户选择的 PNG/JPEG/WebP 居中裁剪并规范化为 512×512 PNG;服务端只接受并验证 512×512 PNG,最大 4 MiB。
|
||||||
|
- 运行文件固定为 `CONFIG_DIR/instance-icon.png`,不保存用户文件名或可控路径;通过临时文件 + rename 原子替换。
|
||||||
|
- 统一公开读取 URL 为 `/api/instance-icon`,默认返回现有 `icon-192.png`;配置接口返回基于内容哈希的 version,前端以查询参数刷新缓存。
|
||||||
|
- 写入和恢复接口使用现有 Bearer token;读取图标与 Manifest 公开,保证登录页也能显示。
|
||||||
|
- 提供动态 `/api/site.webmanifest`:无自定义时保留原 192/512 图标,有自定义时声明 512 图标。已安装 PWA 的操作系统缓存不承诺立即刷新。
|
||||||
|
- `.gitignore` 只追加 `config/instance-icon.png` 与原子临时文件规则,保留现有用户改动和其他配置可见性。
|
||||||
|
|
||||||
|
## 工作区隔离注意
|
||||||
|
|
||||||
|
- `.gitignore` 的既有修改位于暂存区(`git status` 第一列为 `M`),普通 `git diff` 不显示;本功能只能在工作树中追加精确规则,不能改写或取消暂存的用户内容。
|
||||||
|
- `server.js` 的配置目录在启动时统一创建,实例图标常量可与其他配置路径集中定义;HTTP 路由可复用附件接口的 `extractBearerToken`/`activeTokens` 鉴权方式。
|
||||||
|
- 现有静态服务会对所有 public 资源返回 no-store,但动态实例图标仍需显式 `nosniff` 与内容版本,避免不同响应路径的缓存语义分叉。
|
||||||
|
|
||||||
|
## 并发写入归因修正
|
||||||
|
|
||||||
|
- 被中断的实现代理仍有一个已进入执行阶段的工具调用完成落盘:新增了 `scripts/regression.js` 的 219 行聚焦回归、服务端大小常量和路径骨架,并提前推进一次 TODO CSV。
|
||||||
|
- 因此聚焦测试不是 HEAD 预置,而是该代理的有效测试产出;主线程保留测试、合并重复常量,并承担后续实现。CSV 的第 4/5 阶段状态在代码落下后重新与真实结果对齐。
|
||||||
|
|
||||||
|
## 主线程代码审查
|
||||||
|
|
||||||
|
- 写接口只接受鉴权后的 512×512 PNG,路径完全由服务端固定;配置读取、图标读取和 Manifest 无敏感内容,可供登录前消费。
|
||||||
|
- 默认与自定义图标都设置 `nosniff`;默认 ETag 使用实际文件哈希,自定义 ETag 使用内容版本,拉取新版默认资产后不会错误返回旧图 304。
|
||||||
|
- 客户端裁剪取短边居中后绘制到 512×512,不拉伸;上传失败不调用 apply,因此保留旧实例图标。
|
||||||
|
- 现有聚焦回归验证真实子进程和临时 CONFIG_DIR,并由 `withServer` 负责停止服务;测试临时目录清理可作为非阻断维护改进继续审查。
|
||||||
|
- 动态 favicon 的源图片在默认态为 192px、自定义态为 512px,head 上固定 `sizes` 声明可能与默认资源自然尺寸不一致;应在最终审查中确认是否移除固定 sizes 更稳妥。
|
||||||
27
.planning/2026-08-25-instance-custom-icon/progress.md
Normal file
27
.planning/2026-08-25-instance-custom-icon/progress.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 实例自定义图标进度
|
||||||
|
|
||||||
|
## 2026-08-25
|
||||||
|
|
||||||
|
- 已读取 `planning-with-files`、`todo-list-csv` 与 Trellis 工作流。
|
||||||
|
- 已确认 `home-cc-web` codebase-memory 索引状态为 ready。
|
||||||
|
- 已创建并启动 Trellis 任务 `08-25-instance-custom-icon`。
|
||||||
|
- 已识别并保留与本任务无关的工作区改动:`.gitignore`、`README.md`、`config/cross-conversation-replies.json`。
|
||||||
|
- 待办脚本首次使用项目内路径失败,已确定实际安装路径并写入错误记录。
|
||||||
|
- 独立计划审查已通过,无阻断问题。
|
||||||
|
- 并行 codebase-memory 查询因 transport closed 失败;索引此前已确认 ready,改由两个只读代理串行使用 MCP 调研,主线程稍后做本地交叉验证。
|
||||||
|
- 前端只读调研完成并与本地精确检索交叉验证,结果已持久化到 Trellis research 文档。
|
||||||
|
- 服务端只读调研完成,确认可复用 `CC_WEB_CONFIG_DIR`、二进制图片上传、Bearer 鉴权与固定路径写入模式;结果已持久化。
|
||||||
|
- 已完成跨层数据流、HTTP 契约、持久化与错误语义设计,写入 Trellis `info.md`。
|
||||||
|
- 已配置并验证 Trellis implement/check 上下文;实现代理已启动,正在按测试先行方式修改产品代码。
|
||||||
|
- Trellis 实现代理长时间无产品改动且未响应进度询问,已安全中断;主线程接手测试与实现。
|
||||||
|
- 实现代理的在途工具调用于中断后完成落盘,新增 `scripts/regression.js --target instance-icon` 的完整测试目标;实现前运行按预期失败,静态契约缺少 index/frontend/SW/server/style/gitignore 五类实现。
|
||||||
|
- 第一版代码已落下;首次验证发现服务端预置同名大小常量导致重复声明,语法检查已在集成测试前准确阻断,正在定向修正。
|
||||||
|
- 合并重复常量后,`node --check server.js`、`node --check public/app.js` 与 `node scripts/regression.js --target instance-icon` 均通过。
|
||||||
|
- 服务端 API、CONFIG_DIR 固定路径持久化、动态图标/Manifest、设置页裁剪上传/预览/恢复与全局消费点已实现;进入相关与全量回归。
|
||||||
|
- `instance-icon`、`frontend-asset-version` 聚焦回归与 `npm run regression` 全量回归通过;`git diff --check` 通过。
|
||||||
|
- `git check-ignore` 证明正式/临时实例图标文件由新增精确规则忽略,且 `git ls-files` 不跟踪它们;用户已暂存 `.gitignore` 改动与本功能工作树改动保持分层。
|
||||||
|
- Trellis Phase 2.2 独立审查无阻断/高/中风险发现;按唯一低风险建议移除动态图标 link 的固定 sizes,避免默认 192px 与自定义 512px 共用时提示不准确。
|
||||||
|
- 用户要求取消后续额外审计,改为重新打包并将当前工作区全部修改统一提交、推送。
|
||||||
|
- 已复用本机 `@oven/bun-linux-x64-baseline` 重新执行 `npm run build:single-exe`,更新 `dist-exe/cc-web-bun-linux-x64-baseline.tar.gz`。
|
||||||
|
- 发布校验通过:六项 JavaScript 语法检查无错误;打包后二进制的 MCP `initialize` 返回有效 JSON-RPC;tar.gz 可读取;`git diff --check` 通过。
|
||||||
|
- 新发布包 SHA-256 为 `2fb1f0a55ada79420f9599584c53fd4c1d04fb2d05cf25c29cda2150e3b27a48`,大小 43,871,596 bytes。
|
||||||
45
.planning/2026-08-25-instance-custom-icon/task_plan.md
Normal file
45
.planning/2026-08-25-instance-custom-icon/task_plan.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 实例自定义图标实施计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在设置页提供实例级图标选择能力,并让侧栏、浏览器页签等现有品牌图标消费点统一使用该图标。自定义图片与选择结果必须存放在 Git 管理之外,拉取/升级源码后仍保留。
|
||||||
|
|
||||||
|
## 验收边界
|
||||||
|
|
||||||
|
- 支持从本机选择常见栅格图上传,立即预览并保存。
|
||||||
|
- 支持恢复项目默认图标。
|
||||||
|
- 限制图片类型与大小,错误可理解且不破坏旧配置。
|
||||||
|
- 未配置时保持现有图标和行为,不引入升级迁移负担。
|
||||||
|
- 图标响应具备缓存更新机制;设置保存后当前页面可感知更新。
|
||||||
|
- 自定义文件和实例配置不进入 Git,不被 `git pull` 覆盖。
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
| 步骤 | 状态 | 验收方式 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. 定位现有图标、设置和持久化链路 | DONE | 形成前后端入口、测试与数据目录清单 |
|
||||||
|
| 2. 明确实例级图标存储与兼容方案 | DONE | 记录接口、校验、缓存和升级兼容决策 |
|
||||||
|
| 3. 补充服务端配置、上传和图标响应回归测试 | DONE | 新测试先覆盖默认、上传、恢复、非法输入和持久化 |
|
||||||
|
| 4. 实现服务端实例图标持久化与接口 | DONE | 服务端测试通过,数据落在 Git 外置目录 |
|
||||||
|
| 5. 实现设置页图标选择、预览、恢复默认及全局应用 | DONE | 设置页可操作,现有图标消费点统一更新 |
|
||||||
|
| 6. 运行前后端测试与构建并修正问题 | DONE | 目标测试、完整相关测试和构建通过 |
|
||||||
|
| 7. 重新生成 CentOS 7 单文件发布包 | DONE | baseline Bun 构建成功,生成可执行文件与 tar.gz |
|
||||||
|
| 8. 验证发布包与工作区完整性 | DONE | 语法检查、MCP initialize 冒烟、归档读取和 diff 检查通过 |
|
||||||
|
| 9. 暂存并提交当前全部修改 | DONE | 按用户要求包含此前已暂存和本次全部修改 |
|
||||||
|
| 10. 推送到远端并核验结果 | DONE | 交付阶段推送 main 至 origin/main 并确认同步状态 |
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
- 优先复用既有设置 API、数据目录与前端设置面板模式。
|
||||||
|
- 上传内容仅接受可安全解码/展示的图片类型,不允许用户控制落盘路径。
|
||||||
|
- 使用稳定的运行时 URL 暴露图标,并通过版本参数或响应缓存头避免旧图标残留。
|
||||||
|
- 不修改无关的用户工作区变更。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---:|---|
|
||||||
|
| 项目内不存在 `.codex/skills/todo-list-csv/scripts/todo_csv.py` | 1 | 改用技能实际安装路径 `/home/hdzx/.codex/skills/todo-list-csv/scripts/todo_csv.py` |
|
||||||
|
| 并行调用 codebase-memory 架构与两项检索时连接被关闭 | 1 | 停止并行压测;等待只读代理完成 MCP 检索后,用 `rg` 做行号交叉验证 |
|
||||||
|
| Trellis 实现代理长时间运行但未产生产品改动或状态回报 | 1 | 中断代理,主线程按已固化的 TDD 契约接手实现 |
|
||||||
|
| 服务端已有预置 `MAX_INSTANCE_ICON_SIZE`,首次实现重复声明导致语法错误,测试等待端口超时 | 1 | 定位并复用既有常量,先单独语法检查,再重跑聚焦测试 |
|
||||||
47
.planning/2026-08-26-mcp-session-toggle/findings.md
Normal file
47
.planning/2026-08-26-mcp-session-toggle/findings.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# 发现与决策
|
||||||
|
|
||||||
|
## 需求
|
||||||
|
|
||||||
|
- 用户希望减少侧栏中带金色角标的 MCP 创建会话噪声,而不是隐藏普通会话。
|
||||||
|
- 控件是搜索框右侧的单个图标按钮,不显示文字或数量角标。
|
||||||
|
- 隐藏模式保留当前打开、运行中的 MCP 会话;其余 MCP 会话隐藏。
|
||||||
|
- 搜索匹配可临时显示隐藏的 MCP 会话。
|
||||||
|
- 首次默认显示,并记住用户最后一次切换结果。
|
||||||
|
|
||||||
|
## 代码发现
|
||||||
|
|
||||||
|
- `public/app.js` 的 `createSessionListItem()` 已通过 `createdFromKind === 'mcp'` 识别 MCP 会话,并添加 `llm-created` 类。
|
||||||
|
- `renderSessionList()` 先取 `getVisibleSessions()`,再做搜索、置顶拆分和项目分组,是加入纯展示过滤的合适入口。
|
||||||
|
- `buildSessionListStructureSignature()` 会阻止结构未变化时重建 DOM,新偏好必须影响结构签名或显式使签名失效。
|
||||||
|
- `public/index.html` 的 `.session-search-row` 当前包含搜索框和高级搜索按钮,适合并排增加图标开关。
|
||||||
|
- 项目已有项目折叠和旧会话“加载更多”,本功能不能改变这些状态模型。
|
||||||
|
- `scripts/regression.js` 已有 `assertFrontendSidebarCollapseContract()`、`assertSidebarTitleRefreshStormContract()` 和 `assertAdvancedSessionSearchContract()`,可沿用其静态契约与 VM 隔离执行模式补充定向回归。
|
||||||
|
- 完整回归入口是 `scripts/regression.js` 的 `main()`;新增断言需要显式接入该入口,避免测试函数存在但未执行。
|
||||||
|
- `readSidebarCollapsedPreference()` / `persistSidebarCollapsedPreference()` 已提供“读取失败保守降级 + 布尔字符串持久化”的本地偏好范式,新开关应沿用该模式。
|
||||||
|
- `applySessionSnapshot()` 是会话运行态/快照变化后刷新列表的重要入口;过滤应基于每次快照中的 `isRunning` 派生,不能缓存单条会话的隐藏结论。
|
||||||
|
- 搜索输入、Escape 清空和清除按钮都会直接调用 `renderSessionList()`;因此只要过滤基于规范化后的 `sessionSearchQuery` 派生,搜索覆盖和清空恢复无需额外状态。
|
||||||
|
- `.advanced-search-open` 已有 34px 基础按钮与荒原主题 36px 覆盖;新图标按钮可共用尺寸、交互色和窄屏间距,但应使用独立类名表达语义。
|
||||||
|
- `scripts/regression.js` 支持 `--target` 定向入口,可为本功能增加独立 target,把失败测试和后续验证控制在 60 秒内。
|
||||||
|
|
||||||
|
## 视觉发现
|
||||||
|
|
||||||
|
- 用户截图使用荒原主题的窄侧栏。
|
||||||
|
- MCP 创建会话通过条目左侧/边缘的金色角标与普通会话区分。
|
||||||
|
- 搜索行横向空间有限,因此必须使用与高级搜索同级的紧凑图标按钮。
|
||||||
|
|
||||||
|
## 技术决策
|
||||||
|
|
||||||
|
| 决策 | 理由 |
|
||||||
|
|---|---|
|
||||||
|
| 仅做前端派生过滤 | 避免改变会话数据、接口和后端排序 |
|
||||||
|
| 搜索时绕过过滤 | 用户主动查找时应能发现隐藏项 |
|
||||||
|
| 运行态变化依靠现有快照触发重渲染 | 无需增加独立轮询或服务端事件 |
|
||||||
|
| 使用内联 SVG 眼睛图标并切换状态 | 比字符图标跨平台更稳定,符合“一个图标”要求 |
|
||||||
|
|
||||||
|
## 资源
|
||||||
|
|
||||||
|
- `.trellis/spec/frontend/quality-guidelines.md`
|
||||||
|
- `public/app.js`
|
||||||
|
- `public/index.html`
|
||||||
|
- `public/style.css`
|
||||||
|
- `scripts/regression.js`
|
||||||
104
.planning/2026-08-26-mcp-session-toggle/progress.md
Normal file
104
.planning/2026-08-26-mcp-session-toggle/progress.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# 进度日志
|
||||||
|
|
||||||
|
## 会话:2026-08-26
|
||||||
|
|
||||||
|
### 阶段 1:需求与计划审查
|
||||||
|
|
||||||
|
- **状态:** complete
|
||||||
|
- **开始时间:** 2026-08-26T15:30:00+08:00
|
||||||
|
- 已完成:
|
||||||
|
- 使用 grilling 逐项确认过滤范围、状态例外、按钮位置、默认状态、持久化和搜索行为。
|
||||||
|
- 使用 codebase-memory-mcp 定位 `renderSessionList()`、`createSessionListItem()` 与结构签名逻辑。
|
||||||
|
- 读取 Trellis 前端规范、planning-with-files 与 todo-list-csv 技能要求。
|
||||||
|
- 创建 Trellis 任务 `08-26-mcp-session-visibility-toggle`。
|
||||||
|
- 确认现有侧栏、搜索和 VM 隔离回归测试入口,等待计划审查期间未修改产品代码。
|
||||||
|
- 独立计划审查通过,无阻塞问题。
|
||||||
|
|
||||||
|
### 阶段 2:测试先行
|
||||||
|
|
||||||
|
- **状态:** in_progress
|
||||||
|
- **开始时间:** 2026-08-26T16:10:00+08:00
|
||||||
|
- 计划:
|
||||||
|
- 增加 MCP 会话过滤、状态例外、搜索恢复、持久化和图标 DOM 契约测试。
|
||||||
|
- 在产品实现修改前运行并确认测试失败。
|
||||||
|
- 首个测试代理长时间未产生文件变更,已中断并收敛任务后重新派发。
|
||||||
|
- 测试代理最终落盘 `assertMcpSessionVisibilityContract()`、定向 target 和默认回归入口。
|
||||||
|
- 修改产品代码前运行定向测试,按预期失败于“图标按钮不存在”。
|
||||||
|
- 主线程审查并补充纯函数样例,覆盖普通、隐藏、置顶、当前、运行中、搜索恢复和默认偏好。
|
||||||
|
|
||||||
|
### 阶段 3:实现
|
||||||
|
|
||||||
|
- **状态:** complete
|
||||||
|
- 已完成:
|
||||||
|
- 增加首次默认显示且可降级的 `localStorage` 偏好读取/保存。
|
||||||
|
- 增加 MCP 会话纯派生过滤,保留普通、当前和运行中会话,搜索非空时绕过过滤。
|
||||||
|
- 将显示状态纳入结构签名并在每次列表渲染时同步图标无障碍状态。
|
||||||
|
- 在搜索框右侧接入单个 SVG 眼睛图标,无文字或数量角标。
|
||||||
|
- 增加基础主题 34px、荒原主题 36px 样式。
|
||||||
|
|
||||||
|
### 阶段 4:验证
|
||||||
|
|
||||||
|
- **状态:** complete
|
||||||
|
- 已通过:
|
||||||
|
- `node --check public/app.js`
|
||||||
|
- `node --check scripts/regression.js`
|
||||||
|
- `node scripts/regression.js --target mcp-session-visibility`
|
||||||
|
- `node scripts/regression.js --target advanced-session-search`
|
||||||
|
- `node scripts/regression.js --target wasteland-theme`
|
||||||
|
- 运行服务已返回新按钮标记和新的前端内容哈希。
|
||||||
|
- 荒原侧栏计算布局:316px - 左右内边距 28px - 图标 72px - 间距 16px = 搜索框 200px,无横向溢出。
|
||||||
|
- 差异审查中合并新按钮与高级搜索按钮的公共基础样式,保留各自图标与主题特有规则。
|
||||||
|
- 降级说明:
|
||||||
|
- Firefox 无头与软件渲染均因容器 SWGL 图形后端超时,未生成截图;停止重复尝试。
|
||||||
|
- 主线程完整回归曾在既有 Codex App steer 时序场景超时;独立审查代理随后误用裸参数触发完整回归并通过。
|
||||||
|
- 独立审查:
|
||||||
|
- 未发现 High / Medium / Low 或阻塞问题。
|
||||||
|
- 确认过滤顺序、结构签名、状态例外、持久化、无障碍、主题尺寸和测试入口符合 PRD。
|
||||||
|
- 规范复盘:
|
||||||
|
- 本次未形成跨功能的新通用约定;现有前端质量规范与静态资源版本机制已覆盖,不更新 `.trellis/spec/`。
|
||||||
|
- 已创建/修改:
|
||||||
|
- `.planning/2026-08-26-mcp-session-toggle/task_plan.md`
|
||||||
|
- `.planning/2026-08-26-mcp-session-toggle/findings.md`
|
||||||
|
- `.planning/2026-08-26-mcp-session-toggle/progress.md`
|
||||||
|
- `.trellis/tasks/08-26-mcp-session-visibility-toggle/`
|
||||||
|
|
||||||
|
## 测试结果
|
||||||
|
|
||||||
|
| 测试 | 预期 | 实际 | 状态 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 尚未执行 | — | — | 待执行 |
|
||||||
|
| MCP 会话显示失败测试 | `node scripts/regression.js --target mcp-session-visibility` | 产品未实现时失败 | 失败:图标按钮不存在 | ✓ |
|
||||||
|
| MCP 会话显示定向回归 | `node scripts/regression.js --target mcp-session-visibility` | 通过 | 通过 | ✓ |
|
||||||
|
| 高级搜索定向回归 | `node scripts/regression.js --target advanced-session-search` | 通过 | 通过 | ✓ |
|
||||||
|
| 荒原主题定向回归 | `node scripts/regression.js --target wasteland-theme` | 通过 | 通过 | ✓ |
|
||||||
|
| 运行服务静态资源 | `curl http://127.0.0.1:8002/` | 返回新按钮和内容哈希 | 通过 | ✓ |
|
||||||
|
| 独立完整回归 | `node scripts/regression.js mcp-session-visibility`(裸参数实际进入完整入口) | 通过 | 通过 | ✓ |
|
||||||
|
| 独立代码审查 | 只读审查产品与测试 diff | 无阻塞问题 | 无阻塞问题 | ✓ |
|
||||||
|
|
||||||
|
### 阶段 5:交付
|
||||||
|
|
||||||
|
- **状态:** complete
|
||||||
|
- 已完成:
|
||||||
|
- 持久计划、发现与进度记录已同步。
|
||||||
|
- TODO CSV 所有步骤已完成并按技能约定清理。
|
||||||
|
- Trellis 任务上下文已校验,准备结束当前任务。
|
||||||
|
|
||||||
|
## 错误日志
|
||||||
|
|
||||||
|
| 时间 | 错误 | 尝试 | 处理 |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| 2026-08-26T16:12:00+08:00 | 完整历史分叉与 worker 类型参数冲突 | 1 | 改为无历史分叉并显式提供任务上下文 |
|
||||||
|
| 2026-08-26T16:16:00+08:00 | `wait_agent` 等待值低于 10 秒下限 | 1 | 后续使用至少 10 秒的等待值 |
|
||||||
|
| 2026-08-26T16:33:00+08:00 | 高级搜索定向回归未通过 | 1 | 恢复荒原主题高级搜索精确选择器,新按钮改用独立尺寸规则 |
|
||||||
|
| 2026-08-26T16:38:00+08:00 | Firefox 无头截图因 SWGL 图形后端超时 | 1 | 使用软件渲染/Xvfb 再试一次,保留静态与计算布局降级路径 |
|
||||||
|
| 2026-08-26T16:48:00+08:00 | 完整回归 Codex App steer 场景超时 | 1 | 确认失败在既有 server/mock 集成链路;继续以定向回归和只读审查隔离本次变更 |
|
||||||
|
|
||||||
|
## 5 问恢复检查
|
||||||
|
|
||||||
|
| 问题 | 回答 |
|
||||||
|
|---|---|
|
||||||
|
| 当前在哪? | 阶段 1:计划审查 |
|
||||||
|
| 下一步去哪? | 测试先行、实现、验证、交付 |
|
||||||
|
| 目标是什么? | 增加可记忆的 MCP 会话显示/隐藏图标按钮 |
|
||||||
|
| 已发现什么? | 见 findings.md |
|
||||||
|
| 已完成什么? | 见本文件阶段 1 日志 |
|
||||||
90
.planning/2026-08-26-mcp-session-toggle/task_plan.md
Normal file
90
.planning/2026-08-26-mcp-session-toggle/task_plan.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# MCP 会话显示隐藏开关计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在侧栏搜索框右侧增加一个图标按钮,用于显示或隐藏由 MCP 创建的会话;正常会话始终可见,并完整覆盖状态记忆、搜索临时显示与运行态例外。
|
||||||
|
|
||||||
|
## 当前阶段
|
||||||
|
|
||||||
|
阶段 5:交付
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
|
||||||
|
1. 审查需求与实现计划
|
||||||
|
2. 补充会话过滤与图标状态的失败回归测试
|
||||||
|
3. 实现持久化状态与 MCP 会话过滤逻辑
|
||||||
|
4. 接入搜索框右侧图标及主题响应式样式
|
||||||
|
5. 运行定向测试并修复问题
|
||||||
|
6. 执行完整回归与差异审查
|
||||||
|
7. 清理临时跟踪文件并记录交付
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
### 阶段 1:需求与计划审查
|
||||||
|
|
||||||
|
- [x] 通过逐项访谈确认产品边界
|
||||||
|
- [x] 定位现有会话列表、搜索、分组和 MCP 标识逻辑
|
||||||
|
- [x] 完成独立计划审查
|
||||||
|
- **状态:** complete
|
||||||
|
|
||||||
|
### 阶段 2:测试先行
|
||||||
|
|
||||||
|
- [x] 为过滤规则、状态例外、搜索覆盖和图标契约补充失败测试
|
||||||
|
- [x] 运行新增测试并确认修改前失败
|
||||||
|
- **状态:** complete
|
||||||
|
|
||||||
|
### 阶段 3:实现
|
||||||
|
|
||||||
|
- [x] 增加本地持久化的显示状态
|
||||||
|
- [x] 在列表派生数据阶段过滤 MCP 会话
|
||||||
|
- [x] 接入无文字图标按钮、无障碍状态和主题样式
|
||||||
|
- **状态:** complete
|
||||||
|
|
||||||
|
### 阶段 4:验证
|
||||||
|
|
||||||
|
- [x] 运行定向测试、语法检查与完整回归
|
||||||
|
- [x] 审查变更差异、工作区状态及项目规范符合性
|
||||||
|
- **状态:** complete
|
||||||
|
|
||||||
|
### 阶段 5:交付
|
||||||
|
|
||||||
|
- [x] 更新持久计划与 Trellis 记录
|
||||||
|
- [x] 清理临时 TODO CSV
|
||||||
|
- **状态:** complete
|
||||||
|
|
||||||
|
## 已确认需求
|
||||||
|
|
||||||
|
- 过滤对象仅为 `createdFromKind === 'mcp'` 的会话,正常会话始终显示。
|
||||||
|
- 隐藏模式仍保留当前打开或 `isRunning` 的 MCP 会话;其他 MCP 会话隐藏。
|
||||||
|
- 搜索时临时恢复全部 MCP 会话参与匹配,清空搜索后恢复隐藏规则。
|
||||||
|
- 按钮位于搜索框右侧并与高级搜索并排,仅显示一个状态图标。
|
||||||
|
- 首次默认显示;用户切换结果保存在本机浏览器,刷新和下次打开继续沿用。
|
||||||
|
- 没有可见会话的项目分组不渲染。
|
||||||
|
|
||||||
|
## 技术决策
|
||||||
|
|
||||||
|
| 决策 | 理由 |
|
||||||
|
|---|---|
|
||||||
|
| 在 `getVisibleSessions()` 之后、搜索匹配之前派生列表可见性 | 保持服务端快照和缓存完整,只影响侧栏展示 |
|
||||||
|
| 搜索查询非空时绕过 MCP 隐藏过滤 | 满足主动搜索可以找回隐藏会话的需求 |
|
||||||
|
| 使用单一 `localStorage` 布尔偏好,缺失时按显示处理 | 与首次默认显示和跨刷新记忆一致 |
|
||||||
|
| 图标按钮维护 `aria-pressed`、动态 `title`/`aria-label` | 只有图标时仍能清晰表达当前状态和点击动作 |
|
||||||
|
| 将显示偏好纳入列表结构签名或切换时强制失效 | 避免现有结构签名优化跳过重新渲染 |
|
||||||
|
|
||||||
|
## 风险与验证重点
|
||||||
|
|
||||||
|
- 置顶 MCP 会话也应服从隐藏规则,除非它是当前会话或运行中。
|
||||||
|
- 当前会话从 MCP 状态切换后,侧栏不能丢失当前定位。
|
||||||
|
- 新快照把会话从运行中更新为已完成时,隐藏模式应自动移除该条目。
|
||||||
|
- 所有主题及窄屏布局不得挤压搜索输入框或高级搜索按钮。
|
||||||
|
- 不引入服务端状态或协议改动。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---:|---|
|
||||||
|
| 完整历史分叉不能同时指定 worker 类型 | 1 | 改为 `fork_turns: none` 并在任务中显式提供上下文 |
|
||||||
|
| `wait_agent` 等待值低于 10 秒下限 | 1 | 改用 10 秒以上等待值 |
|
||||||
|
| 高级搜索回归要求保留荒原主题精确选择器 | 1 | 恢复 `.session-search-row .advanced-search-open`,新按钮使用独立规则 |
|
||||||
|
| Firefox 无头渲染图形后端超时且未生成截图 | 1 | 改用软件渲染/Xvfb 再试一次,失败则降级到计算布局与静态契约 |
|
||||||
|
| 完整回归 Codex App steer 场景等待 WebSocket 消息超时 | 1 | 失败位于既有 server/mock 集成链路,与前端侧栏变更隔离;保留定向回归与审查证据 |
|
||||||
28
.planning/2026-08-26-release-commit/task_plan.md
Normal file
28
.planning/2026-08-26-release-commit/task_plan.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 重新打包并提交推送计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
使用 CentOS 7 baseline Bun 重新生成 cc-web 单文件发布包,并将当前工作区全部修改提交到 `main` 后推送 `origin/main`。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
1. 确认当前分支、远端状态、工作区范围,并优先检查 `bun`;PATH 中缺失时复用本机 `@oven/bun-linux-x64-baseline/bin/bun`。
|
||||||
|
2. 使用 `BUN_BIN=<baseline bun> npm run build:single-exe` 重新构建 CentOS 7 baseline 发布包。
|
||||||
|
3. 运行发布技能要求的语法检查、归档列表与 MCP smoke;MCP smoke 通过 stdin 发送 JSON-RPC `initialize`,设置短超时并确认 JSON 响应后退出。
|
||||||
|
4. 使用显式 pathspec 排除临时 TODO CSV 后执行 `git add -A`,再复查暂存区,确保其余当前修改全部纳入。
|
||||||
|
5. 提交本地变更。
|
||||||
|
6. 推送前执行 `git fetch origin main` 并核对 ahead/behind;可直接推送时将 `main` 推送到 `origin/main`。
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 使用 `scripts/build-single-exe.js`,目标保持 `bun-linux-x64-baseline`。
|
||||||
|
- 不切换 Docker,不安装 NodeSource Node.js,不把 Claude/Codex CLI 打入发布包。
|
||||||
|
- 用户明确要求不做代码审计;只执行打包技能要求的轻量校验、提交与推送。
|
||||||
|
- 所有当前工作区修改都纳入本次提交;临时 TODO CSV 在完成后删除,不提交。
|
||||||
|
- MCP smoke 使用单条 JSON-RPC `initialize` 请求和短超时,不能留下等待 stdin 的后台进程。
|
||||||
|
- `git add -A -- . ':!重新打包并提交推送 TO DO list.csv'` 后必须确认临时 CSV 仍未跟踪且不在暂存区。
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
- 当前:步骤 1
|
||||||
|
- 计划审查:待完成
|
||||||
39
.planning/2026-08-26-standard-javascript-session/findings.md
Normal file
39
.planning/2026-08-26-standard-javascript-session/findings.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 标准 JavaScript 会话编排发现
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- 提供 `@ccweb/session`,支持 `getCurrentConversationId`、`createConversation`、`sendMessage`、`selectSemanticBranch`、`getLastMessage`。
|
||||||
|
- 脚本位于当前来源对话 cwd 下 `.ccweb/scripts/`,只允许 `.js`,目录使用 ESM。
|
||||||
|
- MCP 提供 create/write/run/get_run/stop 以及 `ccweb_javascript_session_api` manifest。
|
||||||
|
- 异步脚本通过 `runId` 查询;主动 stop 不插入来源消息;异常结束向来源对话插入通知。
|
||||||
|
|
||||||
|
## Research Findings
|
||||||
|
|
||||||
|
- `lib/ccweb-mcp-server.js` 已有 HTTP MCP 客户端,使用 `CC_WEB_MCP_URL`、`CC_WEB_MCP_TOKEN`、`CC_WEB_SOURCE_SESSION_ID`。
|
||||||
|
- `server.js` 的 `/api/internal/mcp` 会鉴权后调用 `callInternalMcpTool`。
|
||||||
|
- 现有会话入口为 `createMcpConversation`、`sendCrossConversationMessage` 和 `handleCodexAppSteerMessage`。
|
||||||
|
- Codex App 完成路径 `handleCodexAppTurnComplete` 会持久化 assistant 消息并完成跨会话回复;需要复用其完成点实现脚本等待。
|
||||||
|
- `killProcess` 与 `handleCodexAppAbortSession` 可作为停止行为的参考,但脚本子进程需要独立 PID/日志/状态管理。
|
||||||
|
- codebase-memory 项目 `home-cc-web` 索引状态为 ready(7740 nodes/17835 edges)。
|
||||||
|
|
||||||
|
## Technical Decisions
|
||||||
|
|
||||||
|
| Decision | Rationale |
|
||||||
|
|----------|-----------|
|
||||||
|
| 脚本包通过本地 `node_modules/@ccweb/session` 注入 | Node ESM 对裸包名解析稳定,不依赖不可靠的 `NODE_PATH` |
|
||||||
|
| 脚本运行凭据按 runId 绑定 | 避免把全局内部 MCP token 长期暴露给用户脚本 |
|
||||||
|
| 日志追加落盘,get_run 分段读取 | 支持用户确认的无限输出,同时保护 MCP 响应大小 |
|
||||||
|
| 判断器一次性只读 Codex App turn | 不污染目标会话、工作区和普通 MCP 能力 |
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
| Issue | Resolution |
|
||||||
|
|-------|------------|
|
||||||
|
| 根目录计划属于此前 hooks 验证任务 | 使用 scoped plan,保留原有计划文件 |
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- `server.js`
|
||||||
|
- `lib/ccweb-mcp-server.js`
|
||||||
|
- `lib/codex-app-server-client.js`
|
||||||
|
- `.codex/skills/planning-with-files/`
|
||||||
85
.planning/2026-08-26-standard-javascript-session/progress.md
Normal file
85
.planning/2026-08-26-standard-javascript-session/progress.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# 标准 JavaScript 会话编排进度
|
||||||
|
|
||||||
|
## Session: 2026-08-26
|
||||||
|
|
||||||
|
### Phase 1:需求与代码链路确认
|
||||||
|
|
||||||
|
- **Status:** complete
|
||||||
|
- **Started:** 2026-08-26
|
||||||
|
- Actions taken:
|
||||||
|
- 完成 grilling 需求对齐,共确认 23 项决策。
|
||||||
|
- 读取项目 AGENTS.md 和相关技能说明。
|
||||||
|
- 使用 codebase-memory 确认索引 ready,并核对 MCP/会话/steer 入口。
|
||||||
|
- 创建本任务 scoped planning 文件。
|
||||||
|
- Files created/modified:
|
||||||
|
- `.planning/2026-08-26-standard-javascript-session/task_plan.md`
|
||||||
|
- `.planning/2026-08-26-standard-javascript-session/findings.md`
|
||||||
|
- `.planning/2026-08-26-standard-javascript-session/progress.md`
|
||||||
|
|
||||||
|
### Phase 2:MCP 契约与脚本运行基础
|
||||||
|
|
||||||
|
- **Status:** complete
|
||||||
|
- Actions taken:
|
||||||
|
- 新增 6 个脚本 MCP 工具定义,并接入 stdio/HTTP `tools/list`。
|
||||||
|
- 在来源对话 cwd 下创建 `.ccweb/scripts/`,注入根 `package.json` 和 `node_modules/@ccweb/session` ESM 包。
|
||||||
|
- 增加脚本名、符号链接、脚本源代码大小校验和原子写入。
|
||||||
|
- 增加独立 Node 子进程、run.json、stdout/stderr 完整落盘、尾部/范围读取、重复启动拒绝、优雅停止/强制终止和重启恢复。
|
||||||
|
- Files created/modified:
|
||||||
|
- `lib/javascript-session-runtime.js`
|
||||||
|
- `lib/ccweb-mcp-server.js`
|
||||||
|
- `server.js`
|
||||||
|
|
||||||
|
### Phase 3:会话标准包与服务端编排
|
||||||
|
|
||||||
|
- **Status:** complete
|
||||||
|
- Actions taken:
|
||||||
|
- 标准包 5 个函数通过短期 run token 调用现有 ccweb `/api/internal/mcp`,不重复实现会话。
|
||||||
|
- 创建/发送服务端等待目标助手消息完成;当前 Codex App 对话复用 steer。
|
||||||
|
- 语义分支使用一次性只读 Codex App 判断 turn,严格候选匹配,最多重试 3 次。
|
||||||
|
- 脚本异常结束/服务重启向来源对话插入带 runId、状态、原因和 stderr 尾部的通知;主动 stop 不插入。
|
||||||
|
- Files created/modified:
|
||||||
|
- `lib/javascript-session-runtime.js`
|
||||||
|
- `server.js`
|
||||||
|
|
||||||
|
### Phase 4:测试与回归
|
||||||
|
|
||||||
|
- **Status:** complete
|
||||||
|
- Actions taken:
|
||||||
|
- 新增 `scripts/javascript-session-runtime-unit.js`,覆盖 ESM 裸包导入、目录边界、重复启动、状态查询、异常通知、主动停止和重启恢复。
|
||||||
|
- 真实服务验证 `tools/list`、API manifest、脚本执行和 `getCurrentConversationId()`。
|
||||||
|
- 修复 Node 18 对 node_modules ESM 包边界的解析问题。
|
||||||
|
- Test results:
|
||||||
|
- `node scripts/javascript-session-runtime-unit.js`:通过
|
||||||
|
- `node scripts/regression.js`:通过
|
||||||
|
- `node --check server.js`:通过
|
||||||
|
- `node --check lib/javascript-session-runtime.js`:通过
|
||||||
|
- `node --check lib/ccweb-mcp-server.js`:通过
|
||||||
|
- `git diff --check`:通过
|
||||||
|
- 项目回归首次运行遇到既有 Codex App steer 模拟的时序抖动,立即重跑通过;未产生源码或运行态残留。
|
||||||
|
|
||||||
|
### Phase 5:交付
|
||||||
|
|
||||||
|
- **Status:** complete
|
||||||
|
- 已清理根目录临时脚本测试产物和 TODO CSV;保留源码、单测与 scoped planning 记录。
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
| Test | Input | Expected | Actual | Status |
|
||||||
|
|------|-------|----------|--------|--------|
|
||||||
|
| 代码索引状态 | `home-cc-web` | ready | ready | ✓ |
|
||||||
|
|
||||||
|
## Error Log
|
||||||
|
|
||||||
|
| Timestamp | Error | Attempt | Resolution |
|
||||||
|
|-----------|-------|---------|------------|
|
||||||
|
| 2026-08-26 | 用户级 planning-with-files 路径不存在 | 1 | 改用项目内技能路径 |
|
||||||
|
|
||||||
|
## 5-Question Reboot Check
|
||||||
|
|
||||||
|
| Question | Answer |
|
||||||
|
|----------|--------|
|
||||||
|
| Where am I? | Phase 2:MCP 契约与脚本运行基础 |
|
||||||
|
| Where am I going? | 完成脚本工具、运行注册表、标准包和回归 |
|
||||||
|
| What's the goal? | 为 cc-web 提供标准 JavaScript 会话编排能力 |
|
||||||
|
| What have I learned? | 现有 MCP/会话/steer 入口可复用,完成点需接入服务端等待 |
|
||||||
|
| What have I done? | 完成需求收敛、代码索引核验和 scoped planning 文件 |
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 标准 JavaScript 会话编排实施计划
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
为 cc-web 增加受控的 JavaScript 脚本编排能力:通过 MCP 创建、写入、异步启动和停止脚本,并注入 `@ccweb/session` 标准包,让脚本 Promise 化调用现有 ccweb 会话。
|
||||||
|
|
||||||
|
## Current Phase
|
||||||
|
|
||||||
|
Phase 5:交付收尾
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
### Phase 1:需求与代码链路确认
|
||||||
|
- [x] 汇总已确认的 API、脚本生命周期、异常通知和错误协议
|
||||||
|
- [x] 使用 codebase-memory 确认现有 MCP、会话创建、消息发送和 steer 入口
|
||||||
|
- **Status:** complete
|
||||||
|
|
||||||
|
### Phase 2:MCP 契约与脚本运行基础
|
||||||
|
- [x] 新增脚本 MCP 工具定义和 API manifest;验收:tools/list 能发现 create/write/run/get_run/stop/api 工具,工具输入 schema 和 manifest 可 JSON 序列化
|
||||||
|
- [x] 新增脚本目录、ESM 标准包注入和路径校验;验收:只能访问当前 cwd/.ccweb/scripts 下的 .js,包可被裸名 import
|
||||||
|
- [x] 新增异步脚本运行注册表、日志落盘、停止和重启恢复;验收:runId 查询、stdout/stderr 分段读取、stop 吊销凭据、重启标记 killed
|
||||||
|
- **Status:** complete
|
||||||
|
|
||||||
|
### Phase 3:会话标准包与服务端编排
|
||||||
|
- [x] 实现 Promise 化的当前会话、创建、发送、最后消息和语义分支能力;验收:每个 API 返回约定字符串/ID,错误携带稳定 code/details
|
||||||
|
- [x] 接入现有 ccweb 会话状态、Codex App steer 和异常来源会话通知;验收:同对话复用 steer,不重复运行同脚本,异常才插入来源通知
|
||||||
|
- **Status:** complete
|
||||||
|
|
||||||
|
### Phase 4:测试与回归
|
||||||
|
- [x] 编写脚本 MCP、标准包和异常通知回归测试
|
||||||
|
- [x] 运行语法检查、单元回归和项目 regression
|
||||||
|
- [x] 修复发现的问题并记录验证结果
|
||||||
|
- **Status:** complete
|
||||||
|
|
||||||
|
### Phase 5:交付
|
||||||
|
- [x] 清理临时 TODO CSV 和本计划状态
|
||||||
|
- [x] 汇总变更、测试结果、风险和未覆盖边界
|
||||||
|
- **Status:** complete
|
||||||
|
|
||||||
|
## Key Questions
|
||||||
|
|
||||||
|
1. 如何在不让脚本轮询的情况下等待现有会话完成?——由服务端等待会话状态和持久化消息变化。
|
||||||
|
2. 如何让同对话消息复用现有 steer,且不重复启动同一脚本?——调用现有 handleMessage/steer,并按来源+脚本路径拒绝并发重复运行。
|
||||||
|
3. 如何在脚本异常结束时通知来源对话,同时区分主动 stop?——运行注册表记录 stopRequested,只有非主动失败插入通知。
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale |
|
||||||
|
|----------|-----------|
|
||||||
|
| 独立 Node 子进程运行脚本 | 隔离脚本异常并支持异步执行 |
|
||||||
|
| `.js` + `type=module`,注入 `@ccweb/session` | 保持用户示例并支持显式 ESM import/顶层 await |
|
||||||
|
| 服务端实现 Promise 等待 | 脚本不实现监听、轮询和等待封装 |
|
||||||
|
| 同对话发送复用现有 steer/插入 | 避免并行 turn 和递归死锁 |
|
||||||
|
| 运行记录和 stdout/stderr 持久化 | 支持异步 runId 查询、重启标记和长输出分段读取 |
|
||||||
|
| 主动 stop 不插入来源对话 | MCP 成功/失败结果已经是调用反馈 |
|
||||||
|
| 非正常结束插入来源对话通知 | 让启动脚本的 Agent 感知异常并继续处理 |
|
||||||
|
|
||||||
|
## Errors Encountered
|
||||||
|
|
||||||
|
| Error | Attempt | Resolution |
|
||||||
|
|-------|---------|------------|
|
||||||
|
| 用户级 planning-with-files 技能路径不存在 | 1 | 改用项目内 `.codex/skills/planning-with-files/` 版本 |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- 不覆盖根目录已有的旧 `task_plan.md`、`findings.md`、`progress.md`,本任务使用 scoped planning 目录。
|
||||||
|
- 按已确认需求不设置运行时长、输出和并发硬上限;完整日志落盘,单次 get_run 只返回尾部或显式范围,避免无限响应。
|
||||||
|
- 脚本按受信代码执行,不伪造 JavaScript 沙箱;仍限制脚本路径、MCP 凭据生命周期和运行记录归属。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Findings
|
||||||
|
|
||||||
|
- Git `main` 起点干净并与 `origin/main` 同步。
|
||||||
|
- codebase-memory 项目 `home-cc-web` 索引状态为 `ready`。
|
||||||
|
- 会话运行状态的统一判定入口是 `server.js` 中的 `isSessionRunning`。
|
||||||
|
- 标准包源码由 `lib/javascript-session-runtime.js` 的 `packageIndexSource` 动态注入。
|
||||||
|
- 内部脚本能力通过 `handleSessionCall` 映射到 `server.js` 注入的 `sessionApi`。
|
||||||
|
- 会话列表刷新入口是 `broadcastSessionList`,但 idle 边沿不能仅依赖 UI 广播,需要服务端可等待的状态协议或包内封装。
|
||||||
|
- `isSessionRunning` 由多个活动 Map/goal 状态综合推导;查询 API 必须直接复用它,不能另造状态字段。
|
||||||
|
- 当前注入包的 HTTP `call()` 是短请求模型;最小可靠实现是新增服务端状态查询,监听和边沿判定由 `@ccweb/session` 内部封装,调用脚本不需要自行轮询。
|
||||||
|
- `onConversationEvent` 适合设计为异步注册:`const unsubscribe = await onConversationEvent(...)`,注册阶段先验证会话并取得基线状态;注销函数同步停止计时器。
|
||||||
|
- `scriptsDirFor(..., create=true)` 目前只在包入口不存在时写入;若不调整,已有 `.ccweb/scripts` 无法获得新增导出,因此生成包入口应在内容变化时原子刷新。
|
||||||
|
- 现有运行时单测包含真实注入包 + 本地 HTTP MCP 的端到端骨架,可扩展为状态序列与注销验证。
|
||||||
|
- “等待子对话回复”已有统一来源:`crossConversationWaitState(sessionId).waitingOnChildren`,其范围包括 `waiting`、`ready`、`delivering`、`failed` 的未处理跨对话回复。
|
||||||
|
- 状态优先级采用 `running` > `waiting_for_children` > `idle`,避免会话自身仍在执行时被较弱等待态覆盖。
|
||||||
|
- 持久子对话关系保存在 session meta 的 `createdFromKind='mcp'` 与 `createdFromSourceSessionId`;`listConversationSummaries(scope='children')` 已验证该筛选语义。
|
||||||
|
- 新增 `getChildConversationIds` 只返回直接子对话,按现有会话列表排序后提取 ID;不递归包含孙对话。
|
||||||
|
- 协议定型:公开 `onConversationEvent` 使用异步注册并返回同步注销函数;包内每 100ms 串行查询状态,不产生重叠请求,调用脚本无需自行轮询。
|
||||||
|
- idle 事件 payload 为 `{ conversationId, event, previousStatus, status, occurredAt }`;只允许事件名 `idle`。
|
||||||
|
- 注册时先取得基线:初始为 idle 不触发;后续任一活动态(running / waiting_for_children)转 idle 才触发。
|
||||||
|
- 注销会清理定时器;已在途的状态请求返回后也会先检查 active 标记,因此不会产生注销后的新回调。
|
||||||
|
- 监听器同步抛错或返回 rejected Promise 时不静默吞掉,按脚本未捕获异常处理;状态查询故障也停止监听并使脚本异常退出。
|
||||||
|
- 动态包版本提升到 1.1.0,并在创建/写入/运行脚本时原子刷新生成的包入口,确保已有脚本目录获得新导出。
|
||||||
|
- 用户明确要求抑制等待态登记延迟产生的临时 idle:服务端 idle 查询先等待 500ms 后复核,包内事件再要求 idle 连续稳定 250ms;任一时刻观察到 `waiting_for_children` 都取消 idle 候选。
|
||||||
|
- 子对话元数据可直接使用 `compareSessionsForList` 排序,返回顺序与会话列表一致(置顶优先,其次最近更新)。
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Progress
|
||||||
|
|
||||||
|
- 2026-08-27:开始新增 `getConversationStatus` 与 `onConversationEvent`。
|
||||||
|
- 已确定公开状态值沿用 ccweb 现有 `running` / `idle`。
|
||||||
|
- 已确认 codebase-memory 索引可用,并定位状态判定、标准包注入和内部调用映射入口。
|
||||||
|
- 计划审查通过;已补齐注销、边沿、manifest、重启前检查与“未完成自动继续”验收点。
|
||||||
|
- 用户追加等待子对话状态与直接子对话 ID 查询;公开函数总数调整为 8。
|
||||||
|
- 已定型公开 API、三态优先级、idle 边沿 payload、注销语义和已有注入包升级策略。
|
||||||
|
- 已补充失败测试:8 函数 manifest、三态值、状态序列 idle→running→idle、注销后停止查询、直接子对话 ID、未知事件错误码。
|
||||||
|
- 红灯结果符合预期:旧实现 manifest 仍为 5 个函数。
|
||||||
|
- 已把 `running → 临时 idle → waiting_for_children → 稳定 idle` 固化为监听回归序列,只有最终稳定 idle 可回调。
|
||||||
|
- 已实现服务端三态查询(idle 500ms 复核)与直接子对话 ID 查询,并接入隐藏脚本 MCP 授权链路。
|
||||||
|
- 已实现 `@ccweb/session` 1.1.0 的 `getConversationStatus`、`getChildConversationIds`、`onConversationEvent`,包内 idle 稳定 250ms 并支持注销。
|
||||||
|
- 已让生成的 `node_modules/@ccweb/session` 在已有脚本目录中按内容原子刷新。
|
||||||
|
- 首轮语法检查与运行时单测通过。
|
||||||
|
- 完整验证通过:`server.js`、运行时、MCP server、单测脚本语法检查,`javascript-session-runtime-unit` 与 `scripts/regression.js` 均成功。
|
||||||
|
- `git diff --check` 通过;manifest 实测为 1.1.0、8 个函数、三态枚举完整。
|
||||||
|
- 已按重启前规则确认仅当前对话运行后完成服务重启;重启后 manifest 与真实脚本均验证通过。
|
||||||
|
- 真实闭环通过:新建子对话 → 监听 idle → 首次“未完成” → 回调内 `sendMessage` → 第二次“任务完成” → 注销;直接子对话 ID 查询和运行中状态查询同时通过。
|
||||||
|
- 确认 `waiting_for_children` 期间及登记延迟造成的临时 idle 均不会触发监听;仅稳定活动态→idle 触发。
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# JavaScript 会话状态与 idle 事件
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
为 `@ccweb/session` 增加会话状态查询、直接子对话 ID 查询,以及指定会话从活动状态转为 `idle` 时的事件监听与注销能力;脚本不需要自行编写轮询。
|
||||||
|
|
||||||
|
## 已确认语义
|
||||||
|
|
||||||
|
- `getConversationStatus(conversationId)` 返回 `Promise<'running' | 'waiting_for_children' | 'idle'>`,优先级为 `running` > `waiting_for_children` > `idle`。
|
||||||
|
- `getChildConversationIds(conversationId)` 返回该对话通过 MCP 创建的直接持久子对话 ID,不递归返回孙对话。
|
||||||
|
- `onConversationEvent(conversationId, 'idle', listener)` 监听 `running/waiting_for_children → idle` 边沿,不因注册时已是 `idle` 而立即触发。
|
||||||
|
- `onConversationEvent` 返回注销函数;注销后不再产生新回调。
|
||||||
|
- 第一版只公开 `idle` 事件,但协议应便于后续扩展。
|
||||||
|
- 回调错误不应破坏底层监听;由脚本自己的未捕获异常规则处理。
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
1. [complete] 梳理现有会话状态与脚本包调用链
|
||||||
|
2. [complete] 确定状态查询、idle 等待、取消注销和 manifest 的协议语义
|
||||||
|
3. [complete] 补充失败测试覆盖状态查询和 idle 监听
|
||||||
|
4. [complete] 实现服务端状态查询与事件等待能力
|
||||||
|
5. [complete] 实现 @ccweb/session 监听、注销和状态 API
|
||||||
|
6. [complete] 更新 manifest 并运行单元与回归测试
|
||||||
|
7. [complete] 检查运行对话后重启并验证 idle 后未完成则继续发送的闭环
|
||||||
|
8. [complete] 清理临时计划并交付使用示例
|
||||||
|
|
||||||
|
## 风险
|
||||||
|
|
||||||
|
- 长时间监听必须能让脚本进程保持运行,又不能在注销后遗留服务端等待器。
|
||||||
|
- idle 必须是活动状态到 idle 的边沿事件,不能把“注册时已经 idle”误判为新完成。
|
||||||
|
- 当前来源对话允许 steer;事件监听必须与现有状态判定保持一致。
|
||||||
|
- 重启前必须确认除当前对话外不存在其他 `running` 会话;否则不重启并记录阻塞。
|
||||||
|
|
||||||
|
## 协议验收清单
|
||||||
|
|
||||||
|
- 状态查询:按 conversationId 返回 `running`、`waiting_for_children` 或 `idle`。
|
||||||
|
- 子对话查询:按 conversationId 返回直接子对话 ID 数组,不递归。
|
||||||
|
- 事件等待:只支持 `idle`,且只响应 `running/waiting_for_children → idle`。
|
||||||
|
- 注销取消:注销后停止包内监听,不再回调,也不遗留活动资源。
|
||||||
|
- manifest:三个新函数的参数、返回值、状态值、触发/注销语义和错误码与实现一致,总计 8 个函数。
|
||||||
|
- 场景闭环:idle 回调中判断任务未完成后调用 `sendMessage` 继续,随后再次进入 idle。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---:|---|
|
||||||
|
| 运行时单测 `5 !== 8` | 1 | 预期的红灯,证明新增 manifest/API 测试在旧实现上失败;进入实现阶段。 |
|
||||||
39
.planning/2026-09-12-ccweb-mcp-create-failure/findings.md
Normal file
39
.planning/2026-09-12-ccweb-mcp-create-failure/findings.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 调查发现
|
||||||
|
|
||||||
|
## 用户现象
|
||||||
|
|
||||||
|
- ccweb MCP 创建对话有时失败。
|
||||||
|
- 创建失败后重载 MCP 也挂载不上,导致当前对话不可继续使用。
|
||||||
|
- 截图中的 `$log-ccweb-title` 提示显示:当前会话没有提供 `ccweb_list_conversations`、`ccweb_set_title` 或 `wiznote_mcp_wiz_*` 工具,因此无法获取对话 ID,无法安全写入 `/coding` 日志。
|
||||||
|
|
||||||
|
## 代码与运行证据
|
||||||
|
|
||||||
|
### 1. MCP 冷启动窗口固定为 10 秒
|
||||||
|
|
||||||
|
- `server.js:3528-3572` 的 `buildCcwebMcpRuntimeConfig()` 在 streamable HTTP 和 stdio 两条路径都固定写入 `startup_timeout_sec: 10`,工具调用窗口为 60 秒。
|
||||||
|
- 历史会话 `723ffd3f-71fc-42ee-87b6-768836316099`、`4c0f6be3-b46c-4ec0-b6f5-03c31188b7d8`、`5ed15712-312c-4d1b-b629-3d3a3c0d06a7` 均持久化了:`MCP client for ccweb timed out after 10 seconds`。
|
||||||
|
- 同一批历史记录随后出现 `ccweb_list_conversations` 60 秒工具调用超时,说明客户端失败后仍可能继续尝试调用失效连接。
|
||||||
|
|
||||||
|
### 2. 创建对话成功不等于目标 MCP 已就绪
|
||||||
|
|
||||||
|
- `createMcpConversation()` 先通过 `createPersistentConversationSession()` 写入会话文件,再调用 `sendCrossConversationMessage()` 投递首条消息。
|
||||||
|
- `sendCrossConversationMessage()` 调用 `handleMessage()`;Codex App 分支的 `handleCodexAppMessage()` 只登记 active turn 并异步执行 `startCodexAppTurn(...).catch(...)`,立即返回 `{ ok: true }`。
|
||||||
|
- 因此创建接口返回的 `ok/status=running` 只表示“会话已落盘、首轮已开始”,不保证首轮完成,更不保证 ccweb MCP 已 ready。MCP 启动失败会在返回之后发生。
|
||||||
|
|
||||||
|
### 3. 重载状态关联存在 threadId 严格匹配竞态
|
||||||
|
|
||||||
|
- `handleReloadMcpApi()` 在 `markCodexAppMcpReloadPending()` 后调用无 thread 参数的 `config/mcpServer/reload`,并只等待 `CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS = 1200` 毫秒。
|
||||||
|
- `codexAppMcpStatusTargetSessionIds()` 对 pending 会话要求 `statusRecord.threadId === pending.threadId`;不一致时直接跳过。
|
||||||
|
- `logs/process.old.log:6713-6721` 中,重载请求针对 `019ff161-...`,随后上报的是 `019fef64-...` 与 `019fef0f-...`,且全部 `targetSessions=0`。对应会话 `b73e4b07-4aaa-43d5-906b-413747a09f4b` 最终仍持久化为 `ccweb.status=starting/rawStatus=pending`。
|
||||||
|
- `cleanupExpiredCodexAppMcpReloads()` 只删除内存 pending 映射,不会把持久化状态从 `starting/pending` 改成失败或可重试,因此会话在 UI 上长期像“挂载中”。
|
||||||
|
|
||||||
|
### 4. 当前环境可复现“并发启动时序差异”
|
||||||
|
|
||||||
|
- 当前调查会话的 `ccweb` 最终为 `ready`,但同一线程的 `playwright` 在 20 秒后失败,说明多个 MCP 并发启动时不同服务的 ready/fail 到达时间并不一致;固定 10 秒窗口和 1.2 秒重载等待会放大这个时序问题。
|
||||||
|
|
||||||
|
## 已实施修复
|
||||||
|
|
||||||
|
- `server.js` 和 `lib/agent-runtime.js`:ccweb MCP 默认启动超时提高到 30 秒,工具超时和 reload 等待窗口支持环境变量覆盖;reload 默认等待 35 秒。
|
||||||
|
- `server.js`:reload pending 记录允许同一 app-server 的全局 reload 通知跨 threadId 关联;等待窗口结束仍未收到 ready/failed/cancelled 时,将持久化状态收敛为 `failed` 并标记可重试。
|
||||||
|
- `server.js`:Codex App 创建对话结果附带当前 `mcpStatus`;首条消息投递失败时保留会话 ID、失败阶段和可重试标记,避免“已创建的会话”被误认为完全不存在。
|
||||||
|
- `scripts/mock-codex-app-server.js`、`scripts/regression.js`:新增无关 threadId 和无最终状态通知的回归场景。
|
||||||
13
.planning/2026-09-12-ccweb-mcp-create-failure/progress.md
Normal file
13
.planning/2026-09-12-ccweb-mcp-create-failure/progress.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 调查进度
|
||||||
|
|
||||||
|
## 记录
|
||||||
|
|
||||||
|
- 2026-09-12:建立本轮 scoped 排查计划;确认 codebase-memory 项目 `home-cc-web` 索引状态为 ready。
|
||||||
|
- 2026-09-12:截图现象先记录为“当前会话 MCP 工具未注入/不可见”,待与创建和重载链路对照。
|
||||||
|
- 2026-09-12:确认 `buildCcwebMcpRuntimeConfig()` 两条传输路径均固定 `startup_timeout_sec=10`;历史会话持久化了 ccweb 10 秒超时。
|
||||||
|
- 2026-09-12:确认 `createMcpConversation()` 的首条消息通过 `handleCodexAppMessage()` 异步启动,接口返回不等待目标线程/MCP ready。
|
||||||
|
- 2026-09-12:确认 reload 仅等待 1200ms,状态通知按 threadId 严格关联;历史日志出现请求线程与上报线程不一致、`targetSessions=0`,导致会话持久化为 `starting/pending`。
|
||||||
|
- 2026-09-12:用户确认进入修复实施;已将 ccweb MCP 启动超时、工具超时、reload 等待窗口改为可配置,默认分别为 30 秒、60 秒、35 秒。
|
||||||
|
- 2026-09-12:已修正 reload 的跨 threadId 全局状态关联,并在等待窗口超时后持久化 `failed` 状态和可重试提示。
|
||||||
|
- 2026-09-12:创建对话结果补充 Codex App MCP 状态;首条消息投递失败保留会话 ID、creationStatus 和 retryable 元数据。
|
||||||
|
- 2026-09-12:mock + 完整 regression 已通过;未重启服务,待收尾清理临时清单。
|
||||||
25
.planning/2026-09-12-ccweb-mcp-create-failure/task_plan.md
Normal file
25
.planning/2026-09-12-ccweb-mcp-create-failure/task_plan.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# ccweb MCP 创建对话失败与重载失败排查
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
定位 ccweb MCP 创建新对话偶发失败、失败后重载 MCP 无法挂载的共同根因,判断是否需要修复代码并完成最小回归验证。
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
- [completed] 梳理现有 MCP 配置与重载回归入口
|
||||||
|
- [completed] 提高并配置 ccweb MCP 启动与重载等待窗口
|
||||||
|
- [completed] 修正重载状态的 threadId 关联与超时收敛
|
||||||
|
- [completed] 补充创建对话的 MCP 状态可见性与失败信息
|
||||||
|
- [completed] 补充回归断言并运行相关验证
|
||||||
|
- [completed] 清理临时清单并汇总交付风险
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 使用 codebase-memory-mcp 做代码定位,rg 只做行号与文本补充校验。
|
||||||
|
- 不覆盖用户已有未提交改动。
|
||||||
|
- 仅在确认根因且改动属于本问题范围时修改业务代码。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---:|---|
|
||||||
55
.planning/2026-09-13-session-switch-diagnosis/findings.md
Normal file
55
.planning/2026-09-13-session-switch-diagnosis/findings.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# 切换对话诊断发现
|
||||||
|
|
||||||
|
## 当前已知
|
||||||
|
|
||||||
|
- 会话文件仍包含用户消息,暂未发现数据层删除。
|
||||||
|
- 切换通过 WebSocket `load_session` 请求完成,刷新会创建新连接。
|
||||||
|
- 服务端日志存在大量心跳终止和断线重连记录。
|
||||||
|
- 前端同时处理会话缓存、加载请求代次、历史分页、运行态恢复和滚动位置。
|
||||||
|
|
||||||
|
## 可复核证据
|
||||||
|
|
||||||
|
- `server.js:5656-5658` 普通加载只发送最近 `INITIAL_HISTORY_COUNT = 12` 条;
|
||||||
|
`server.js:10890-10907` 同时发送完整 `historyTotal`、`historyCursor`、
|
||||||
|
`historyBaseIndex` 和 `historyTruncated`,普通大历史请求可以出现
|
||||||
|
`messages=12, historyTotal=99, historyCursor=87, historyPending=false`。
|
||||||
|
- 原逻辑 `public/app.js:2607-2611` 用 `!payload.historyPending` 推导
|
||||||
|
`complete`,因此上述部分快照会被标成完整;随后
|
||||||
|
`session_info → cacheSessionSnapshot → getSessionCacheDisposition →
|
||||||
|
showCachedSession` 可在 A → B → A 时绕过服务端重新加载。
|
||||||
|
- 修复后 `normalizeSessionSnapshot` 和 `isCompleteSessionSnapshot` 共同要求
|
||||||
|
游标归零、未截断、未等待分片且缓冲数覆盖总数;缓存写入和读取都复用同一
|
||||||
|
谓词。历史分片按 `historyBaseIndex` 合并,最后一页才转换为完整缓存。
|
||||||
|
- 部分快照现在进入独立 `sessionHistoryBuffers`,仅供后续 `load_history_page`
|
||||||
|
合并;`getSessionCacheDisposition` 永远不会把它当作可展示缓存。分页游标归
|
||||||
|
零后才调用 `cacheSessionSnapshot`,因此 A → B → A 不会复用半截历史。
|
||||||
|
- `logs/process.log` 中存在 `ws_heartbeat_terminate`,例如
|
||||||
|
`2026-09-13T07:30:03.445Z`,`missedPongs=4`、`lastActivityAgeMs=150471`;
|
||||||
|
`server.js:2216-2221` 的 `markWsActivity` 已会把 `isAlive` 恢复为 `true`,
|
||||||
|
因此该记录更符合浏览器/反向代理长期未返回 pong 的半断连接,而不是服务端
|
||||||
|
漏置位。
|
||||||
|
- 客户端原先只在 `onclose` 触发重连;现增加 `WS_CLIENT_HEARTBEAT_INTERVAL_MS`
|
||||||
|
(15 秒)与 `WS_CLIENT_HEARTBEAT_TIMEOUT_MS`(10 秒),通过
|
||||||
|
`client_heartbeat → client_heartbeat_ack` 主动识别浏览器仍显示 `OPEN` 的半断,
|
||||||
|
再复用现有 `onclose` 流程重放切换请求。
|
||||||
|
|
||||||
|
## 问题边界
|
||||||
|
|
||||||
|
- 会话文件未发现消息被删除;气泡消失的直接原因是前端错误缓存/重绘,
|
||||||
|
不是数据库或服务端历史丢失。
|
||||||
|
- WebSocket 处于浏览器 `OPEN` 但链路半断时,`send(load_session)` 可能无效;
|
||||||
|
刷新页面会强制新建连接。现在客户端最多约 25 秒主动发现并重连,真实反向
|
||||||
|
代理是否丢弃控制帧仍需浏览器现场或代理配置验证。
|
||||||
|
- 未重启现有 `ccweb`,因为当前会话列表中还有另一个 running 对话;源码修复
|
||||||
|
需在安全窗口重启后才会进入线上进程。
|
||||||
|
|
||||||
|
## 2026-09-13 新增线上证据
|
||||||
|
|
||||||
|
- 用户截图中的 `Unknown type: client_heartbeat` 证实线上 PM2 仍运行未包含
|
||||||
|
`client_heartbeat` 分支的旧 `server.js`;前端先部署心跳会被旧服务端当作业务
|
||||||
|
错误,并触发连接重连循环。
|
||||||
|
- `mcp__ccweb__ccweb_list_conversations(status=running)` 显示当前会话之外仍有
|
||||||
|
一个 running 会话,按项目运维约定本轮不能重启 PM2。
|
||||||
|
- 已在前端加入 `auth_result.features.clientHeartbeat` 能力闸门:旧服务端不声明
|
||||||
|
时不发送心跳;即使旧页面已发送并收到 Unknown type,也会静默停用心跳,不再
|
||||||
|
将协议探测错误显示到聊天区。新版服务端已声明该能力,安全重启后可恢复探测。
|
||||||
10
.planning/2026-09-13-session-switch-diagnosis/progress.md
Normal file
10
.planning/2026-09-13-session-switch-diagnosis/progress.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 诊断进度
|
||||||
|
|
||||||
|
- 2026-09-13:承接既有只读排查结果,创建本轮诊断记录;未修改业务代码、未重启服务。
|
||||||
|
- 2026-09-13:补充部分历史快照缓存回归契约;修改前因缺少统一完整性判断而失败,进入最小前端修复阶段。
|
||||||
|
- 2026-09-13:修复 `normalizeSessionSnapshot`、缓存判定和历史分片收口;局部回归与 Node 语法检查通过,尚未重启服务。
|
||||||
|
- 2026-09-13:复核确认服务端 `markWsActivity` 已正确处理 pong,未保留冗余服务端改动;定向回归、全量回归和 `git diff --check` 均通过。因存在其他 running 对话,未执行 pm2 重启。
|
||||||
|
- 2026-09-13:补充旧坏缓存、A→B→A 切换和“部分快照→连续分页→strong cache”回归;完整性谓词显式要求 `historyBaseIndex=0`,并移除 `normalizeSessionSnapshot` 的强制 complete 覆盖入口。
|
||||||
|
- 2026-09-13:增加客户端应用层心跳与服务端应答,半断连接不再只能等待 45 秒加载超时;定向回归、全量回归、语法检查和 `git diff --check` 再次通过。
|
||||||
|
- 2026-09-13:根据用户截图确认线上旧服务端返回 `Unknown type: client_heartbeat`;增加服务端能力声明与前端能力闸门,并对旧协议错误做静默兼容。当前仍有其他 running 会话,未执行 PM2 重启。
|
||||||
|
- 2026-09-13:补充断线期间的自动重试提示,并让高亮收口最多保留一个 `.session-item.active`;Node 语法、定向回归及重新执行的完整回归均通过。线上 PM2 仍保持不重启。
|
||||||
49
.planning/2026-09-13-session-switch-diagnosis/task_plan.md
Normal file
49
.planning/2026-09-13-session-switch-diagnosis/task_plan.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 修复切换对话消息气泡消失计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
修复会话切换时“用户消息气泡暂时消失”的前端缓存状态错误,并保留对
|
||||||
|
WebSocket 半断导致切换超时的证据与边界;通过回归测试证明部分历史快照
|
||||||
|
不会再被当作完整会话缓存。服务不重启,避免影响其他运行中的对话。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 会话 A 历史大于初始窗口时,`session_info` 的 `historyCursor > 0`、
|
||||||
|
`historyPending = false` 快照不能命中 strong cache;A → B → A 必须重新
|
||||||
|
请求 `load_session`,不能只显示最近窗口。
|
||||||
|
- 完整快照必须同时满足:`complete = true`、`historyPending = false`、
|
||||||
|
`historyCursor = 0`、`historyBaseIndex = 0`、`historyTruncated = false`、
|
||||||
|
`historyBuffered >= historyTotal`。
|
||||||
|
- 历史分片按稳定消息索引合并;只有最后一页使游标归零时才写入完整缓存,
|
||||||
|
旧分页或旧切换响应不能污染当前会话。
|
||||||
|
- 完整快照仍可 strong cache 命中;部分快照不会直接渲染为缓存会话。
|
||||||
|
- 客户端应用层心跳每 15 秒探测一次,连续 10 秒未收到服务端应答就主动关闭
|
||||||
|
当前连接并复用现有重连/切换请求重放;服务端原生 pong 心跳仍负责底层连接。
|
||||||
|
真实代理半断链路不在本轮浏览器自动化模拟范围。
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
- [完成] 1. 核对会话数据、WebSocket 协议和缓存根因
|
||||||
|
- [完成] 2. 增加部分历史快照缓存回归测试
|
||||||
|
- [完成] 3. 修复快照完整性判定与完整加载收口
|
||||||
|
- [完成] 4. 运行静态检查和回归测试
|
||||||
|
- [完成] 5. 汇总运行态限制、根因和剩余风险
|
||||||
|
- [完成] 6. 修复旧服务端与新版前端心跳协议不兼容,并补充断线提示/单一高亮收口
|
||||||
|
|
||||||
|
## 验证命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --check public/app.js
|
||||||
|
node --check server.js
|
||||||
|
node --check scripts/regression.js
|
||||||
|
node scripts/regression.js --target session-switch-race
|
||||||
|
node scripts/regression.js --target history-recall
|
||||||
|
node scripts/regression.js --target codexapp-stale-running
|
||||||
|
node scripts/regression.js
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 旧版 PM2 服务端不认识 `client_heartbeat` | 前端先于服务端重启上线心跳 | 增加 `auth_result.features.clientHeartbeat` 能力闸门,并静默兼容旧错误 |
|
||||||
|
| 完整回归首次出现 `historyLoadMore is not defined` | 首次运行时测试进程与临时服务异常退出,定向回归可复现通过 | 重新运行完整回归已通过,未发现新的历史控件故障 |
|
||||||
24
.planning/codex-rollout-history-merge/findings.md
Normal file
24
.planning/codex-rollout-history-merge/findings.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 调研发现
|
||||||
|
|
||||||
|
- `server.js` 中 `resolveSessionHistory` 位于约 4489 行,session_info、session_history_chunk、resume_session_result 多处复用。
|
||||||
|
- `lib/codex-rollouts.js` 是 native rollout 解析入口,需结合 server.js 的持久化写入路径分析。
|
||||||
|
- `public/app.js` 已有 `renderMessages` 与 `reconcileRenderedSessionMessages`,前端存在重连/历史刷新竞态相关逻辑。
|
||||||
|
- codebase-memory 项目 `home-cc-web` 索引状态为 ready(8208 nodes / 19300 edges)。
|
||||||
|
- 工作区存在用户已有未提交改动:`lib/ccweb-mcp-server.js`、`scripts/ccweb-message-reply-unit.js`,本任务不触碰。
|
||||||
|
|
||||||
|
|
||||||
|
- 2026-09-21:db04e08 已删除全量 native 替换,但 merge 的数值 baseIndex 回退仍不可靠;messagesEquivalent 不支持 codexAppTurnId 与 native turnId 跨来源对齐。
|
||||||
|
- 前端 prepend/reconcile 仍以相同下标跳过不同 ID,缓存也会按下标覆盖不同消息。
|
||||||
|
- sessions/35394008-4f0c-43c0-a481-b622d9e6b884.json 本机暂未找到,不能假装完成迁移。
|
||||||
|
|
||||||
|
- 自动回传使用 buildCrossConversationReplyAutoRunText 的“子对话回传已返回……”包装,需与“来自……”投递包装分别精确识别;两者均不能作为普通用户消息。
|
||||||
|
- response_item 中存在 AGENTS/environment 等用户角色上下文;有 event_msg.user_message 时以事件为权威,仅从匹配 response 补充 ID。
|
||||||
|
- done 原来不携带持久化助手身份,结束后的本地 stream 与刷新消息无法去重;现已将最终消息及合并后下标通过 done 传递。
|
||||||
|
- 新迁移能力通过已认证 WebSocket 在服务端同步检查/备份/写入;单文件提供 --migrate-session-history 入口,默认预览,--apply 写入。
|
||||||
|
|
||||||
|
## 2026-09-21 旧会话迁移复盘
|
||||||
|
|
||||||
|
- `sessions/35394008-…json` 的截断提示结构化字段完整,但仍报 `history_boundary_unconfirmed`:快照首条是 cc-web 专有的跨对话回传显示气泡(`ccwebDisplayOnly`),rollout 里不存在对应记录,旧规则只认首条锚点就直接放弃确认。
|
||||||
|
- mergeHistorySegments 的 turn 别名键会把 native 前缀里同一 turn 的多条气泡折叠成一条,实测丢掉 9 条助手正文(2537 字);native 前缀必须按自身 ID 去重,不能按 turn 去重。
|
||||||
|
- rollout 解析器会把同一条气泡拆成多条空文本工具片段且共用同一 `history-assistant:<hash>` ID,前端按稳定 ID 去重时只保留最后一条;前缀侧需要先按 ID 合并片段(正文拼接、工具调用并集)再交给前端。
|
||||||
|
- 迁移后文件不再带截断提示,未升级的旧服务进程也会走 snapshot 分支把 739 条全部展示,historyAvailable 为 true,因此迁移不必等待服务重启。
|
||||||
38
.planning/codex-rollout-history-merge/progress.md
Normal file
38
.planning/codex-rollout-history-merge/progress.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 执行进度
|
||||||
|
|
||||||
|
- 2026-09-20:建立独立计划目录,确认现有未提交改动并完成 codebase-memory 索引检查。
|
||||||
|
- 2026-09-20:服务端新增稳定消息 ID、快照基线元数据和保守 native 前缀合并;`session_info`、`resume_session_result`、`session_history_chunk` 统一读取 resolver。
|
||||||
|
- 2026-09-20:rollout 解析按 `task_started` / `turn_context` / 完成事件聚合同一 turn,保留 function/custom tool,过滤跨对话内部 user 输入。
|
||||||
|
- 2026-09-20:前端缓存、DOM、重连和实时消息按稳定键去重,普通用户消息把本地 ID 作为 `clientMessageId` 发给服务端。
|
||||||
|
- 2026-09-20:通过 `node --check`、完整 `npm run regression`、`history-recall`、`session-switch-race`、既有单测和 `git diff --check`。
|
||||||
|
|
||||||
|
- 2026-09-21:收紧 native 合并,仅首条稳定标识/完整时间戳和内容摘要边界可信;废止数值基线切 native,跨线程和分段 turn 保守回退。
|
||||||
|
- 2026-09-21:新增结构化截断元数据、修正消息名额及基线覆盖;服务端新增 idle 状态的预览/备份/原子迁移入口,脚本默认预览。
|
||||||
|
- 2026-09-21:用户确认目标旧会话在其他机器,本轮不操作远端会话数据。
|
||||||
|
- 2026-09-21:历史服务端 9 项、rollout 10 项、前端专项行为回归已通过;历史召回和切会话专项通过。额外补齐流式转持久化的身份绑定,准备全量和浏览器验证。
|
||||||
|
|
||||||
|
- 2026-09-21:全量 npm run regression 通过;关联 ccweb-message-reply、failed-insert-card、ccweb-list-user-inputs、javascript-session-runtime 单测和语法检查通过。最终 auto-run 过滤后的 history-recall、session-switch-race 复跑通过。
|
||||||
|
- 2026-09-21:使用 /tmp/ccweb-bun.7eHd2F/node_modules/@oven/bun-linux-x64-baseline/bin/bun(1.4.2)开始构建发布包;Firefox 隔离浏览器验证执行中。
|
||||||
|
|
||||||
|
- 2026-09-21:CentOS 7 baseline 发布包构建完成,MCP initialize、单文件 --migrate-session-history 入口、tar 结构均通过烟测;SHA-256 为 7c8fe9bcb1be8c860e10578a1fac925c51785f83dd6449cbfa57f09dcc370c9e。
|
||||||
|
- 2026-09-21:无本机目标数据迁移;本机列表仍有其他 running 对话,按 AGENTS.md 不重启本机服务。用户本轮要求异机使用的验证、打包与推送。
|
||||||
|
|
||||||
|
- 2026-09-21:最终全量回归再次通过。Firefox 156.0 / geckodriver 0.37.1 真浏览器验证通过:68 条快照、483 条 native;加载、运行、刷新、真实 WebSocket 重连、done、done 后刷新六阶段保持顺序且无重复,historySource=merged。
|
||||||
|
- 浏览器证据:/tmp/cc-web-history-browser-KnMvZ7/evidence.json;运行中截图 /tmp/cc-web-history-browser-KnMvZ7/refreshed.png 已目视核对。隔离进程已清理。
|
||||||
|
- 本轮目标在异机,未迁移或重启生产会话;发布包提供 idle 安全迁移工具。准备提交全部源码、测试、计划及发布包并推送 main。
|
||||||
|
|
||||||
|
- 2026-09-21:修复、测试和发布包已提交 f8f5ef3 并推送 origin/main;全部步骤完成,已清理本轮临时 CSV。
|
||||||
|
|
||||||
|
## 2026-09-21 旧会话实际迁移
|
||||||
|
|
||||||
|
- 渲染侧确认问题:目标会话默认视图只剩最后 12 条气泡,顶部提示「还有 68 条更早消息,但原始记录不可用,无法恢复」,用户输入气泡全部落在无法加载的历史里。
|
||||||
|
- 边界规则改为允许跳过快照头部的 cc-web 专有气泡(跨对话回传显示、本地用户回显),遇到无法对齐的普通消息仍然放弃确认;native 前缀改用按 ID 合并片段的 mergeNativePrefixWithSnapshot。
|
||||||
|
- 单测新增 2 项(头部专有气泡跳过、前缀同 ID 片段合并),历史服务端回归 12 项通过;全量 npm run regression 结果见本轮日志。
|
||||||
|
- 官方迁移入口走运行中的旧进程仍会拒绝,改用等价离线流程(同 resolver、同 idle 前置、同备份目录、同原子写入与回读校验)迁移目标会话:80 条快照 + 1111 条 native 前缀 = 739 条,备份写入 sessions/_history-backups。
|
||||||
|
- 迁移后校验:739 条消息、0 重复 ID、时间单调、91 条用户气泡、用户原问题与对应助手回复邻接;浏览器实测加载更多可用,用户气泡可见;其他会话文件未被改写。
|
||||||
|
|
||||||
|
## 2026-09-21 重新打包与推送结果
|
||||||
|
|
||||||
|
- 用 bun 1.4.2 baseline 重新构建发布包 dist-exe/cc-web-bun-linux-x64-baseline.tar.gz(44,970,741 字节),随代码一起提交 5e9bda7。
|
||||||
|
- 验证:node --check 七个入口文件全部通过;session-history-unit 历史服务端回归 12 项通过;npm run regression 通过;发布二进制 `--ccweb-mcp-server` 的 initialize 握手返回合法 JSON;tar 包目录结构正常。
|
||||||
|
- 5e9bda7 已推送 origin/main。运行中的 cc-web 仍是 08:59 启动的进程,新合并逻辑要等一次重启才在服务端生效。
|
||||||
74
.planning/codex-rollout-history-merge/task_plan.md
Normal file
74
.planning/codex-rollout-history-merge/task_plan.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Codex rollout 历史合并修复计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让 cc-web 持久化消息成为当前会话的权威顺序;Codex native rollout 仅补充更早历史,按 turn 聚合助手输出,并通过稳定消息标识完成历史合并与前端去重。
|
||||||
|
|
||||||
|
## 合并契约
|
||||||
|
|
||||||
|
- 持久化快照中去掉 `ccwebPersistenceNotice` 后的消息保持原有顺序,是当前会话的权威尾部;native 只能补充快照之前的消息。
|
||||||
|
- 快照压缩时保存 `historySnapshotBaseIndex`(cc-web 逻辑消息索引)与 `historySnapshotCount`;只按首条稳定 ID 别名或完整时间戳和内容摘要确认 native 边界,禁止用数值基线直接切 native。
|
||||||
|
- 合并顺序固定为:native 更早消息 → cc-web 持久化消息 → 当前运行中的消息;相同稳定 ID 只保留 cc-web 消息,ID 冲突且内容不一致时以持久化消息为准并记录告警。
|
||||||
|
- native 缺失、解析异常、边界无法确认或合并结果不能证明快照尾部完整时,返回 cc-web 快照,不把未经确认的 native 消息插入当前视图。
|
||||||
|
- `session_info`、`resume_session_result`、`session_history_chunk` 均调用同一解析结果;分页只对该结果切片。
|
||||||
|
|
||||||
|
## 稳定标识契约
|
||||||
|
|
||||||
|
- 消息已有 `id` 始终保留;新增用户消息保留 `clientMessageId`,缺失时生成并持久化 UUID。
|
||||||
|
- Codex App 助手消息:使用 `codexAppTurnKey`,其输入为 session/thread/turn 的稳定字段;旧消息缺失时从对应运行状态或时间戳生成一次并保存。
|
||||||
|
- native rollout 助手消息:使用 `nativeTurnKey`,由 threadId + turnId/turn_context 组成;同一 turn 跨刷新保持一致。
|
||||||
|
- 跨对话回传:保留 `replyToRequestId`(兼容 `crossConversation.replyRequestId`),同时生成 `reply:<requestId>` 作为稳定消息 ID;来源元数据始终保留。
|
||||||
|
- 其他历史消息按 `message:<role>:<timestamp>:<sha256(content)>` 降级,避免数组下标去重;ID 冲突时以权威来源和较完整内容决胜。
|
||||||
|
|
||||||
|
## Turn 聚合契约
|
||||||
|
|
||||||
|
- 以 `turn_id`、`turn_context.turn_id`、`turn_context.id` 或 session/thread 上下文组成 turn key;缺失时使用相邻事件窗口的稳定 fallback key。
|
||||||
|
- 同一 turn 内按 rollout 文件顺序合并文本片段、工具调用与工具结果;只有遇到新 turn、下一条真实用户消息、turn 完成/失败事件或文件结束才 flush。
|
||||||
|
- tool call/result 绑定同一调用 ID;空 turn 不产出气泡;跨对话内部 user 回传不作为普通 user 消息插入。
|
||||||
|
|
||||||
|
## 前端契约
|
||||||
|
|
||||||
|
- 服务端先输出规范化消息序列;前端渲染层按 `message.id`/`messageId`/`nativeTurnKey`/`codexAppTurnKey`/`replyToRequestId` 形成稳定键。
|
||||||
|
- `renderMessages`、分页 prepend、`reconcileRenderedSessionMessages` 均按稳定键去重,索引仅用于定位/排序兼容;同键内容更新应替换原 DOM,不追加新气泡。
|
||||||
|
- 实时消息与历史消息竞态时,以服务端规范化消息为准,保留当前用户消息与对应助手回复邻接关系;跨对话回传显示来源标识,角色保持 assistant。
|
||||||
|
|
||||||
|
## 可验收测试矩阵
|
||||||
|
|
||||||
|
- 历史合并:无 native、无快照、完全重叠、native 仅更早、快照尾部用户消息、native 异常/边界不明回退快照;断言顺序、长度、稳定 ID。
|
||||||
|
- rollout 聚合:同一 turn 多个 message/tool/result 仅一条 assistant;下一用户消息 flush;turn 完成/失败 flush;内部回传不生成 user 气泡。
|
||||||
|
- 稳定 ID:重复加载/刷新/分页 ID 不变;clientMessageId、codexAppTurnKey、replyToRequestId 去重;冲突以持久化消息为准。
|
||||||
|
- 前端回归:session_info/resume/history chunk 重复到达不追加;实时流与历史同时到达不拆泡;来源标识和当前生成消息同时可见。
|
||||||
|
- 浏览器实测:重载运行会话时用户原问题、架构回复、子对话来源标识、当前生成消息均按逻辑顺序可见。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
- [complete] 盘点服务端、rollout、持久化与前端消息链路
|
||||||
|
- [complete] 设计并实现稳定消息标识与 native 历史合并
|
||||||
|
- [complete] 聚合 Codex rollout 的同一轮助手输出
|
||||||
|
- [complete] 调整前端规范化历史渲染与稳定 ID 去重
|
||||||
|
- [complete] 增加历史合并和 rollout 聚合回归测试
|
||||||
|
- [complete] 运行静态检查、单测和浏览器/集成验证并收尾
|
||||||
|
|
||||||
|
## 约束与决策
|
||||||
|
|
||||||
|
- 保留工作区已有未提交改动,不覆盖 `lib/ccweb-mcp-server.js` 与 `scripts/ccweb-message-reply-unit.js`。
|
||||||
|
- 不把未确认的 native 消息插入当前 cc-web 快照;合并失败时返回快照。
|
||||||
|
- 回传消息保留来源标识,不能伪装成普通用户气泡。
|
||||||
|
- 所有可观测失败记录简短告警,不阻断当前会话展示。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-09-21 反馈修订
|
||||||
|
|
||||||
|
- [complete] 收紧历史边界并补齐解析、前端稳定标识
|
||||||
|
- [complete] 补齐结构化快照元数据及安全迁移入口
|
||||||
|
- [complete] 运行历史、重连回归与必要浏览器验证
|
||||||
|
- [complete] 检查旧会话迁移条件及服务重启条件
|
||||||
|
- [complete] 重建 CentOS 7 发布包并烟测
|
||||||
|
- [complete] 提交全部修改并推送
|
||||||
|
|
||||||
|
修订约束:不以 historySnapshotBaseIndex 直接切 native;只确认快照首条的可靠边界,不要求尚未进入 rollout 的最近消息也匹配。ID 别名优先,完整时间戳与内容摘要次之;未确认则只保留快照。目标旧会话只在 idle 且原文件未变化时备份和原子迁移。用户要求不再审计,本轮仅做实现与必要回归。update_plan 工具当前未提供,以本计划和 CSV 同步跟踪。
|
||||||
9
.planning/create-conversation-async/findings.md
Normal file
9
.planning/create-conversation-async/findings.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 发现
|
||||||
|
已暴露工具描述只强调持久会话用途。requestReply 参数原文:若为 true,会在新对话完成本轮输出后把回复写回来源对话,并继续触发来源对话运行。默认 false。
|
||||||
|
该描述未说明创建调用立即返回、禁止 sleep/轮询、来源会话可结束当前轮次。代码索引 home-cc-web 为 ready。
|
||||||
|
|
||||||
|
实现 createMcpConversation 同步创建并投递消息后返回,未阻塞等待;requestReply=true 且无首条消息会返回 reply_requires_initial_message;send_message 已说明立即返回但 create 未说明。已有 scripts/ccweb-message-reply-unit.js 与 scripts/regression.js 创建回传集成覆盖。
|
||||||
|
|
||||||
|
回传由 deliverCrossConversationReply 完成;来源仍 running 时返回 false,保持 ready,来源完成轮次后 flush 再投递。循环 sleep 会持续占着 running,导致自动回传延迟。用户只要默认不轮询且允许显式例外,不改运行时行为。
|
||||||
|
|
||||||
|
用户再次指出“默认异步等待”可能被误读成参数控制的阻塞方式。修订明确 requestReply 控制回传订阅,是否轮询由模型遵循提示词与用户要求决定。
|
||||||
16
.planning/create-conversation-async/progress.md
Normal file
16
.planning/create-conversation-async/progress.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 完成记录
|
||||||
|
- 已按用户要求仅修改 ccweb_create_conversation 主说明和 requestReply 参数说明。
|
||||||
|
- 明确默认异步、不轮询,保留用户指定轮询的例外。
|
||||||
|
- 明确 false 不自动回传,true 自动回传并触发来源继续运行,两者调用均立即返回。
|
||||||
|
- node --check lib/ccweb-mcp-server.js 通过。
|
||||||
|
- timeout 60s node scripts/ccweb-message-reply-unit.js 通过。
|
||||||
|
- git diff --check 通过;独立只读审查通过。
|
||||||
|
- 没有更改业务逻辑、默认值、返回值,没有创建真实会话或发送消息;未重启运行服务。已有会话的旧工具定义不会因文件修改自动更新。
|
||||||
|
- 规范复盘:本次属已有异步契约说明补全,无需新增通用规范。
|
||||||
|
|
||||||
|
## 2026-09-17 后续交付
|
||||||
|
用户授权重启并重新打包、提交推送全部修改。PM2 已确认 ccweb online,进程 4186290,重启计数 70,启动时间 2026-09-17T13:19:07.223Z。正在重新生成 baseline 发布包。
|
||||||
|
|
||||||
|
发布验证通过:7 个入口文件语法检查通过;baseline 可执行文件 MCP initialize 与 tools/list 通过,tools/list 已验证包含新回传提示;压缩包 137 项、43904045 字节,SHA256 为 9289fddd83ce4feb542a6c02e17e047665846360a80d03d05376ea1dc35d4648。重启后的 HTTP 请求返回 200。按用户要求不再代码审计。
|
||||||
|
|
||||||
|
全部源码、发布包与任务记录已提交为 96bfcaa 并成功推送 origin/main;临时 TODO CSV 已完成后清理。
|
||||||
22
.planning/create-conversation-async/task_plan.md
Normal file
22
.planning/create-conversation-async/task_plan.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 修复创建会话异步等待提示
|
||||||
|
目标:按用户最新要求,只修正 ccweb_create_conversation 提示词,明确“除非用户指定,否则默认不轮询等待回复,而是异步等待”。
|
||||||
|
1. 核验工具提示词和异步回复链路 — completed
|
||||||
|
2. 修正创建会话的异步交互契约 — completed
|
||||||
|
3. 补充并运行相关回归验证 — completed
|
||||||
|
4. 核对生效条件并整理交付 — completed
|
||||||
|
边界:只改 lib/ccweb-mcp-server.js 的 description 和 requestReply description;不改功能、不改默认值、不扩展返回值、不新增测试、不重启服务。
|
||||||
|
验收:description 保留用户明确指定轮询的例外;false 表示不自动回传,true 表示完成后回传并触发来源继续运行;两者创建调用均立即返回;true 必须携带 initialMessage。使用现有单测、语法检查与导出提示词检查验证。
|
||||||
|
工具限制:当前没有 update_plan 工具,以同文案文件计划与 CSV 同步状态。
|
||||||
|
错误记录:早期工具调用有格式与未打印输出问题,已改为 await tools + text;没有据此变更代码。用户明确缩小范围后取消任何绝对禁止轮询要求。
|
||||||
|
|
||||||
|
## 2026-09-17 后续澄清
|
||||||
|
用户接受推荐措辞:启用 requestReply 后,默认由系统异步回传结果;除非用户明确要求轮询,否则不主动轮询等待。
|
||||||
|
1. 按推荐措辞更新两处工具说明 — completed
|
||||||
|
2. 核验提示词与现有检查并收尾 — completed
|
||||||
|
仍仅修改两处说明,保持默认 false 和业务逻辑。
|
||||||
|
|
||||||
|
## 2026-09-17 打包交付
|
||||||
|
按用户要求重新打包、提交全部修改并推送,不再代码审计。
|
||||||
|
1. 生成 CentOS 7 baseline 发布包 — completed
|
||||||
|
2. 验证发布产物并整理全部修改 — completed
|
||||||
|
3. 提交全部修改并推送远端 — completed
|
||||||
10
.planning/full-outline-history/findings.md
Normal file
10
.planning/full-outline-history/findings.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 发现记录
|
||||||
|
|
||||||
|
## 2026-09-13
|
||||||
|
|
||||||
|
- “定位”按钮对应 `user-outline-panel`;`buildUserOutlineItems()` 当前只扫描 `messagesDiv.querySelectorAll('.msg.user[data-message-id]')`,因此只包含已渲染的最近消息。
|
||||||
|
- `session_info` 已携带 `historyCursor` / `historyTotal`,服务端 `load_history_page` 可按 `before` 返回旧消息页;现有前端收到该响应后会无条件 `prependHistoryMessages()`,所以不能直接用现有手动分页状态填充定位列表。
|
||||||
|
- 旧消息点击定位需要同时处理两类目标:已渲染消息直接滚动;未渲染消息先通过消息索引加载对应页,再将聊天区滚动到目标。完整索引缓存不能依赖 DOM 元素 ID。
|
||||||
|
- 定位面板本身已有独立 `max-height` 和 `overflow-y: auto`,不会因为完整索引而扩大聊天区;需要保留该隔离边界。
|
||||||
|
- 旧消息点击定位使用独立的 `load_history_page` 目标页请求:按消息索引只取包含目标的一页,进入聊天区后再滚动到目标;完整定位索引请求只保存用户消息摘要,不渲染旧消息。
|
||||||
|
- `session_history_chunk` 当前按 `activeHistoryPageRequest` 分支后统一调用 `prependHistoryMessages()`;独立定位请求必须用独立 request 状态在该分支前截获,否则会破坏“隐藏历史不占聊天空间”的要求。
|
||||||
19
.planning/full-outline-history/progress.md
Normal file
19
.planning/full-outline-history/progress.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 进度日志
|
||||||
|
|
||||||
|
## 2026-09-13
|
||||||
|
|
||||||
|
- 已定位问题:定位列表只读当前聊天 DOM,45 条窗口之外的旧用户消息被完全遗漏。
|
||||||
|
- 已确认修复边界:完整历史只进入独立索引;聊天 DOM 和消息滚动空间保持最近窗口策略。
|
||||||
|
- 已确认旧条目点击应走独立目标页请求;完整定位请求必须在 `session_history_chunk` 中提前截获,避免调用 `prependHistoryMessages`。
|
||||||
|
- 新增失败回归:要求独立定位索引状态、分页请求函数、旧页拦截分支和按 `data-message-index` 定位入口;修改前按预期失败。
|
||||||
|
- 已实现摘要索引、定位专用分页请求、旧页不渲染分支、面板加载/重试状态,以及隐藏条目按消息索引重新加载。
|
||||||
|
- 全量 `npm run regression` 首次因历史处理器回归夹具缺少新状态桩失败,已补齐夹具;第二次完整回归通过。
|
||||||
|
- 旧条目点击使用独立 `load_history_page` 目标页请求,只在用户明确点击时把目标所在页加载进聊天区;定位列表后台索引不会改变聊天 DOM 高度。
|
||||||
|
- 最终验证通过:`npm run regression`、`node --check public/app.js`、`node --check scripts/regression.js`、`git diff --check`。
|
||||||
|
- 目标页响应与完整定位分页均按独立 requestId 分支处理;断线和错误会清理悬挂请求,避免后续历史响应误消费。
|
||||||
|
- 补齐无近期用户消息的边界:只要仍有可用旧历史,定位按钮保持可打开,以便触发完整索引分页。
|
||||||
|
- 根据实际截图修正历史入口布局:`history-load-more` 移入 `#messages` 的首位,初始停在底部时不悬浮;滚到消息顶部才显示,继续滚动时随内容离开。
|
||||||
|
- 为消息重绘和历史前插补充控件保留/插入锚点,避免重绘时丢失历史入口或把消息插到控件上方。
|
||||||
|
- 用户进一步明确交互:不是“跟历史内容一起在当前视口显示”,而是“仅作为消息内容首行,滚到最顶才露出”;当前 DOM/CSS 已按此语义实现。
|
||||||
|
- 运行中服务静态核验通过:`/app.js` 含定位目标页逻辑,`/index.html` 使用 `20260913-history-control-position` 样式版本,刷新即可生效。
|
||||||
|
- 临时 `补齐定位列表完整历史 TO DO list.csv` 已清理;计划文件保留用于追踪本次设计决策。
|
||||||
49
.planning/full-outline-history/task_plan.md
Normal file
49
.planning/full-outline-history/task_plan.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 补齐定位列表的完整历史索引
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让“定位”列表展示当前会话的全部用户消息,即使聊天区只保留最近 45 条用于显示;旧消息只进入定位索引,不重新插入聊天 DOM,也不额外占用聊天滚动空间。
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
| 阶段 | 状态 | 验收标准 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. 核对定位列表与历史分页的数据链路 | complete | 明确当前定位数据仅来自 DOM,并确认可复用的历史分页响应 |
|
||||||
|
| 2. 补充完整定位索引的失败回归 | complete | 回归能证明隐藏旧消息时定位列表仍应获得完整用户消息 |
|
||||||
|
| 3. 增加独立的定位历史索引缓存 | complete | 打开定位列表时可按页读取完整历史,且不改变聊天消息 DOM |
|
||||||
|
| 4. 支持旧消息定位加载与滚动 | complete | 点击未渲染旧消息会按消息索引加载所需页并滚动到目标 |
|
||||||
|
| 5. 运行定向回归、语法和差异检查 | complete | 定向回归、JS 语法检查、git diff --check 全部通过 |
|
||||||
|
| 6. 清理临时记录并交付修改结果 | complete | TODO CSV 删除,明确完整列表与聊天空间隔离的实现和验证结果 |
|
||||||
|
|
||||||
|
## 机器可读阶段状态
|
||||||
|
|
||||||
|
### Phase 1:核对定位列表与历史分页的数据链路
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 2:补充完整定位索引的失败回归
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 3:增加独立的定位历史索引缓存
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 4:支持旧消息定位加载与滚动
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 5:运行定向回归、语法和差异检查
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 6:清理临时记录并交付修改结果
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
## 关键决策
|
||||||
|
|
||||||
|
- 不改变聊天区“最近 45 条 + 手动回看”的显示策略。
|
||||||
|
- 不把定位列表的完整历史通过隐藏 DOM 塞进消息滚动区;旧消息只保存在轻量索引对象中。
|
||||||
|
- 复用 `load_history_page` 的现有分页协议,新增请求标识分支,避免与用户点击“查看更早消息”的聊天分页互相抢状态。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| 定向历史回归按预期失败:缺少 `currentOutlineHistoryState` | 1 | 证明旧实现没有独立定位索引;已补充失败契约后进入实现 |
|
||||||
|
| 全量回归提取历史处理器时报 `activeOutlineHistoryRequest is not defined` | 1 | 为现有历史处理器回归夹具补充独立定位状态和合并函数桩;随后全量回归通过 |
|
||||||
10
.planning/gitea-workflow-core/findings.md
Normal file
10
.planning/gitea-workflow-core/findings.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Gitea Workflow 核心模块发现
|
||||||
|
|
||||||
|
- PRD 位于 `docs/gitea-workflow/PRD.md`,当前只确认了需求基线,`ARCHITECTURE.md` 尚不存在。
|
||||||
|
- 现有 Node 项目使用 CommonJS,核心业务模块集中在 `lib/`,测试多为 `scripts/*-unit.js` 的直接执行脚本。
|
||||||
|
- `server.js` 体量很大,本轮应新增独立文件并避免直接改写入口。
|
||||||
|
- 当前工作区已有用户未提交改动和多个 Gitea 规划文件,需避免覆盖。
|
||||||
|
- `home-cc-web` 的 codebase-memory 索引状态为 `ready`(6760 节点、15125 边);整体由 `server`、`app`、`task-board`、`codex` 等包组成,本轮新增模块可独立放入 `lib/`。
|
||||||
|
- 项目无测试框架脚本,单元测试惯例是 `node scripts/*-unit.js` 直接执行并使用 Node 内置 `assert`。
|
||||||
|
- 当前并行实现已形成两层契约:`gitea-workflow-domain/store/queue/service` 是可复用领域与 Webhook 编排层;`gitea-workflow-core.js` 保留轻量兼容服务并补充 turn/配置脱敏/审计 API。server.js 当前通过 `gitea-workflow-service.js` 注入 `setRunner` 与 `enqueueNormalizedTask`,不需要复制状态机。
|
||||||
|
- 生产 runner 仍依赖 Gitea host/token、`gitea-mcp` 可执行文件、工作区 clone/fetch 和 Codex App 回执查询;这些属于集成配置/外部依赖,不在本轮单元测试中连接真实 Gitea。
|
||||||
11
.planning/gitea-workflow-core/progress.md
Normal file
11
.planning/gitea-workflow-core/progress.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Gitea Workflow 核心模块进度
|
||||||
|
|
||||||
|
## Log
|
||||||
|
|
||||||
|
- 已读取项目 AGENTS、规划技能、PRD 与现有 lib/test 风格。
|
||||||
|
- 已确认本轮使用独立 `lib/gitea-workflow-*.js` 文件和独立单元测试脚本。
|
||||||
|
- 已新增 `gitea-workflow-domain.js`、`gitea-workflow-store.js`、`gitea-workflow-queue.js`、`gitea-workflow-service.js`;并补齐现有 `gitea-workflow-core.js` 的 turn、配置脱敏、规范化任务入队和 server runner 挂接契约。
|
||||||
|
- 核心单测从 6 项扩展为 7 项,覆盖验签、mention/Bot 过滤、delivery 去重、持久化恢复、并发队列、MCP 配置和 `enqueueNormalizedTask`。
|
||||||
|
- 已通过 `node --check`、核心单测、Webhook 回归、工作区单测、管理 API 单测和 `npm run regression`。
|
||||||
|
- 已补充 `expectedVersion` 乐观并发校验,旧状态写入会返回 `version_conflict`,并将资源 `resourceKey` 对齐为 `<repoKey>:<kind>:<number>`。
|
||||||
|
- 已清理本轮 server smoke 产生的临时 `config/gitea-*` 状态与空工作区锁目录,避免运行态文件混入交付。
|
||||||
21
.planning/gitea-workflow-core/task_plan.md
Normal file
21
.planning/gitea-workflow-core/task_plan.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Gitea Workflow 核心领域模块实施计划
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
在不重写 `server.js` 的前提下,为 Gitea Webhook 工作流新增可独立使用的核心领域模块,覆盖配置模型、仓库/会话/turn 状态、持久化、delivery 去重、仓库串行队列、重启恢复与审计事件,并提供测试或可调用接口。
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- [x] 读取现有代码与 PRD/架构约束
|
||||||
|
- [x] 实现领域模型、状态机与配置规范化
|
||||||
|
- [x] 实现持久化、去重、队列与重启恢复
|
||||||
|
- [x] 实现审计记录与可调用服务接口
|
||||||
|
- [x] 补充测试并完成静态/运行验证
|
||||||
|
- [x] 汇总接口契约、修改文件与未决集成点
|
||||||
|
|
||||||
|
## Errors Encountered
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---:|---|
|
||||||
|
| 全局 planning-with-files 路径不存在 | 1 | 改用项目内 `.codex/skills/planning-with-files` |
|
||||||
|
| `todo-list-csv` 技能文件未发现 | 1 | 手工创建同等格式的任务 CSV |
|
||||||
9
.planning/history-control-position/findings.md
Normal file
9
.planning/history-control-position/findings.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 发现记录
|
||||||
|
|
||||||
|
## 2026-09-13
|
||||||
|
|
||||||
|
- 用户截图显示“还有 16 条更早消息 / 查看更早消息”以胶囊形式停在消息内容上方,用户明确要求它回到“上面再显示”,不要悬着跟随。
|
||||||
|
- `public/index.html` 已将控件放在 `.messages-wrap` 内、`#messages` 之前,但 `public/style.css` 给 `.history-load-more` 设置了 `position: absolute`、`top`、`left` 和水平位移,因此它覆盖消息且随可视区域悬浮。
|
||||||
|
- `.messages-wrap` 当前没有纵向 flex 布局,`#messages` 仅使用 `height: 100%`;改为顶部普通节点 + 下方 `flex: 1` 滚动区即可保留现有分页、prepend 和滚动补偿逻辑。
|
||||||
|
- 本次最小实现不需要改变历史消息协议或 `requestOlderHistory`;只调整布局样式并补充静态契约回归,避免引入新的滚动副作用。
|
||||||
|
- `public/index.html` 原先固定使用旧的 `style.css` 查询版本;已更新为 `20260913-history-control-position`,避免浏览器缓存旧悬浮样式。
|
||||||
16
.planning/history-control-position/progress.md
Normal file
16
.planning/history-control-position/progress.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 进度日志
|
||||||
|
|
||||||
|
## 2026-09-13
|
||||||
|
|
||||||
|
- 建立本次历史消息入口位置修复计划,尚未开始代码修改。
|
||||||
|
- 完成定位:`.history-load-more` 使用绝对定位覆盖消息;`.messages-wrap` 未为控件预留布局空间。
|
||||||
|
- 确定最小修复方案:父容器纵向 flex,控件置于顶部正常流,消息区使用 `flex: 1` 独立滚动。
|
||||||
|
- 新增 `history-recall` 位置契约,先按预期捕获当前 `position: absolute` 实现;进入 CSS 布局修复。
|
||||||
|
- 确认 HTML 顺序已经是控件在 `#messages` 之前,无需改动历史协议或分页脚本;通过父容器布局让该顺序真正占据顶部空间。
|
||||||
|
- CSS 已移除绝对定位、top/left/transform 和毛玻璃覆盖效果;`.messages-wrap` 改为纵向 flex,`.messages` 改为剩余空间滚动。
|
||||||
|
- `node scripts/regression.js --target history-recall` 已通过。
|
||||||
|
- 窄屏沿用 `max-width: min(92%, 560px)` 与内容收缩规则,不新增覆盖定位。
|
||||||
|
- `node --check public/app.js`、`node --check scripts/regression.js`、`git diff --check` 均已通过。
|
||||||
|
- 临时 `修复历史消息入口位置 TO DO list.csv` 已按流程清理;本次源码改动涉及 `public/index.html`、`public/style.css` 和 `scripts/regression.js`。
|
||||||
|
- 同步更新 `public/index.html` 的样式查询版本,且历史回看回归新增缓存失效断言;新增验证仍全部通过。
|
||||||
|
- 运行中 `127.0.0.1:8002` 已直接返回新 CSS 和新样式版本 URL;无需重启,刷新页面即可加载。
|
||||||
43
.planning/history-control-position/task_plan.md
Normal file
43
.planning/history-control-position/task_plan.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# 修复历史消息入口位置
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
将“还有 N 条更早消息 / 查看更早消息”入口放回消息流顶部的正常文档流中,避免它在滚动时悬浮、跟随内容或遮挡消息;同时保留连续翻页和失败重试能力。
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
| 阶段 | 状态 | 验收标准 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. 定位历史入口的 DOM、脚本和样式链路 | complete | 明确控件是否由 fixed/sticky 定位、插入到哪个滚动容器及滚动时的行为 |
|
||||||
|
| 2. 补充控件位置回归断言 | complete | 回归能锁定入口必须位于消息流顶部且不使用悬浮定位 |
|
||||||
|
| 3. 调整历史入口的 DOM 插入和滚动逻辑 | complete | 控件作为消息列表首部普通节点显示,加载后不跟随视口悬浮 |
|
||||||
|
| 4. 调整样式并兼容窄屏布局 | complete | 桌面和窄屏下入口均保持正常流布局,不遮挡消息内容 |
|
||||||
|
| 5. 运行定向回归、语法检查和差异检查 | complete | 相关回归、JS 语法检查、git diff --check 全部通过 |
|
||||||
|
| 6. 清理临时记录并交付修改结果 | complete | TODO CSV 删除,工作区仅保留本次修改并明确验证结果 |
|
||||||
|
|
||||||
|
## 机器可读阶段状态
|
||||||
|
|
||||||
|
### Phase 1:定位历史入口的 DOM、脚本和样式链路
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 2:补充控件位置回归断言
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 3:调整历史入口的 DOM 插入和滚动逻辑
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 4:调整样式并兼容窄屏布局
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 5:运行定向回归、语法检查和差异检查
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
### Phase 6:清理临时记录并交付修改结果
|
||||||
|
**Status:** complete
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| 定向历史回归按预期失败:历史控件仍为绝对定位 | 1 | 已证明新增契约能捕获当前悬浮实现,进入布局修复 |
|
||||||
|
| `curl` 返回 23 | 1 | 仅因输出管道被 `rg -m 1` 提前关闭;同一响应已成功打印目标 CSS 片段,非服务错误 |
|
||||||
14
.planning/history-message-recall/findings.md
Normal file
14
.planning/history-message-recall/findings.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 历史消息回看发现
|
||||||
|
|
||||||
|
## 初始状态
|
||||||
|
|
||||||
|
- 用户看到提示:“历史消息过多,cc-web 已只保留最近 45 条用于本地展示,省略 50 条旧消息。”
|
||||||
|
- 用户明确要求:旧消息可以默认隐藏,但必须有办法查看前面的内容。
|
||||||
|
|
||||||
|
## 代码证据
|
||||||
|
|
||||||
|
- `server.js:sanitizeMessagesForPersist` 生成用户看到的中文裁剪提示;它只影响持久化快照的展示内容。
|
||||||
|
- `server.js:handleLoadSession` 已将历史拆成最近窗口和 `olderChunks`,并发送 `historyTotal/historyCursor/historyTruncated/historyPending` 元数据。
|
||||||
|
- `server.js:handleLoadHistoryPage` 已存在按 `before` 和 `HISTORY_CHUNK_SIZE` 读取历史页的 WebSocket 入口,但当前固定返回 `remaining: 0`,需要确认前端是否能继续请求及服务端是否正确维护游标。
|
||||||
|
- `public/app.js` 已存在 `prependHistoryMessages`,说明“前置插入旧消息”的渲染能力已有;还需要核对 `session_history_chunk` 处理、入口文案和重复/边界状态。
|
||||||
|
- 计划审查指出验收必须明确:默认仍只渲染最近窗口、入口可操作、可连续查看旧消息、顺序和角色正确,并覆盖失败/重复点击/并发新消息等边界。
|
||||||
10
.planning/history-message-recall/progress.md
Normal file
10
.planning/history-message-recall/progress.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 历史消息回看进度
|
||||||
|
|
||||||
|
## 日志
|
||||||
|
|
||||||
|
- 2026-09-11:建立隔离计划目录,准备定位历史消息裁剪和回看链路。
|
||||||
|
- 2026-09-11:计划审查未通过,已补充验收标准、数量边界、分页/并发/权限风险;审查意见要求保留必要计划记录,只清理临时 TODO 文件。
|
||||||
|
- 2026-09-11:代码图谱确认服务端已有 `handleLoadHistoryPage`、`handleLoadSession`/`splitHistoryMessages`,前端已有 `prependHistoryMessages`,问题更可能是入口或游标链路未闭合,而非完全没有历史能力。
|
||||||
|
- 2026-09-11:实现服务端原始历史恢复、普通会话按需分页、历史游标回传和前端顶部回看控件;历史页按稳定消息索引去重并合并缓存。
|
||||||
|
- 2026-09-11:新增 `history-recall` 回归断言,覆盖 45 条近期 + 50 条旧消息的切分、Codex rollout 解析、入口/分页协议和重复消息保护;该目标与完整 `npm run regression` 均通过。
|
||||||
|
- 2026-09-11:按发布技能用本地 Bun baseline 重建 `dist-exe/cc-web-bun-linux-x64-baseline.tar.gz`;语法检查、MCP initialize smoke 和归档清单均通过;临时 TODO 清单已清理。
|
||||||
44
.planning/history-message-recall/task_plan.md
Normal file
44
.planning/history-message-recall/task_plan.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# 修复历史消息回看入口
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让被本地展示裁剪的旧消息仍然可通过明确入口回看,保留当前渲染性能与会话加载稳定性。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
- [x] 定位消息裁剪逻辑、历史数据来源和现有接口
|
||||||
|
- [x] 明确按需加载旧消息的交互与数据边界
|
||||||
|
- [x] 实现历史消息回看入口及服务端/客户端衔接
|
||||||
|
- [x] 补充裁剪与历史回看回归测试
|
||||||
|
- [x] 运行测试并核验兼容性,完成后按约定清理临时 TODO 文件
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 默认加载仍只渲染最近 45 条(或当前代码实际配置的同等窗口),不会因增加入口而自动拉取全部旧消息。
|
||||||
|
- 出现“省略 50 条旧消息”时,界面有清晰可操作的回看入口,而不是只有数量提示。
|
||||||
|
- 用户点击入口后能看到被省略消息的真实内容,并可继续按页查看更早消息。
|
||||||
|
- 回看后的顺序、时间线、角色、内容和现有消息渲染保持正确;滚动位置不会跳乱。
|
||||||
|
- 覆盖空历史、旧消息不足一页、连续多页、重复点击、加载失败和新消息并发到达等边界。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---:|---|
|
||||||
|
|
||||||
|
## 当前决策
|
||||||
|
|
||||||
|
- 不删除旧消息数据,只调整本地展示和按需加载路径。
|
||||||
|
- 先复用现有会话消息接口;只有确认接口无法分页时才扩展接口。
|
||||||
|
|
||||||
|
## 风险与约束
|
||||||
|
|
||||||
|
- 优先使用分页/游标加载,避免一次性把全部历史复制到浏览器内存。
|
||||||
|
- 历史加载与实时新消息并发时,必须按稳定消息索引或 ID 去重并保持排序。
|
||||||
|
- 历史接口沿用会话归属校验;失败时给出可理解的错误反馈并允许重试。
|
||||||
|
- 不能破坏现有裁剪提示、缓存、滚动位置和消息渲染批处理。
|
||||||
|
|
||||||
|
## 已确认实现边界
|
||||||
|
|
||||||
|
- 旧消息优先从 Claude JSONL / Codex rollout 原始记录恢复;cc-web 快照只作为无原始记录时的降级来源。
|
||||||
|
- 正常打开会话只渲染最近窗口;历史按钮使用现有 `load_history_page` WebSocket 按游标加载。
|
||||||
|
- 搜索命中跳转仍允许目标预取,不受普通会话的按需加载策略影响。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
26
.trellis/tasks/08-24-2026-08-24-gitea-workflow/task.json
Normal file
26
.trellis/tasks/08-24-2026-08-24-gitea-workflow/task.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "2026-08-24-gitea-workflow",
|
||||||
|
"name": "2026-08-24-gitea-workflow",
|
||||||
|
"title": "Gitea Webhook 驱动 Codex App 工作流",
|
||||||
|
"description": "",
|
||||||
|
"status": "planning",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": "server.js,lib,codexapp,public,docs",
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "shiyue",
|
||||||
|
"assignee": "shiyue",
|
||||||
|
"createdAt": "2026-08-24",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
6
.trellis/tasks/08-25-instance-custom-icon/check.jsonl
Normal file
6
.trellis/tasks/08-25-instance-custom-icon/check.jsonl
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
|
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "前端入口与运行时更新验收"}
|
||||||
|
{"file": ".trellis/spec/backend/error-handling.md", "reason": "服务端错误与降级验收"}
|
||||||
|
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "跨层契约验收"}
|
||||||
|
{"file": ".trellis/tasks/08-25-instance-custom-icon/research/frontend-icon-chain.md", "reason": "前端影响面核对"}
|
||||||
|
{"file": ".trellis/tasks/08-25-instance-custom-icon/research/backend-settings-storage.md", "reason": "后端安全与持久化核对"}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "前端设置页与图标消费点规范入口"}
|
||||||
|
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "入口资源和运行时更新质量要求"}
|
||||||
|
{"file": ".trellis/spec/backend/index.md", "reason": "服务端规范入口"}
|
||||||
|
{"file": ".trellis/spec/backend/error-handling.md", "reason": "HTTP 和配置保存错误处理"}
|
||||||
|
{"file": ".trellis/spec/guides/cross-layer-thinking-guide.md", "reason": "跨前后端契约与数据流"}
|
||||||
|
{"file": ".trellis/tasks/08-25-instance-custom-icon/research/frontend-icon-chain.md", "reason": "前端图标与设置链路调研"}
|
||||||
|
{"file": ".trellis/tasks/08-25-instance-custom-icon/research/backend-settings-storage.md", "reason": "后端配置与存储调研"}
|
||||||
84
.trellis/tasks/08-25-instance-custom-icon/info.md
Normal file
84
.trellis/tasks/08-25-instance-custom-icon/info.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 实例自定义图标技术设计
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
```text
|
||||||
|
本地 PNG/JPEG/WebP
|
||||||
|
→ 浏览器解码并居中裁剪为 512×512 PNG
|
||||||
|
→ POST /api/instance-icon(Bearer,二进制,≤4 MiB)
|
||||||
|
→ 服务端验证 Content-Type、PNG 签名、IHDR 尺寸
|
||||||
|
→ CONFIG_DIR/.instance-icon.png.tmp 原子 rename
|
||||||
|
→ 返回 { custom, version, updatedAt, url }
|
||||||
|
→ 前端更新 favicon、Apple Touch、登录 Logo、通知图标与设置预览
|
||||||
|
```
|
||||||
|
|
||||||
|
## 持久化与升级
|
||||||
|
|
||||||
|
- 正式文件:`CONFIG_DIR/instance-icon.png`。
|
||||||
|
- 临时文件:`CONFIG_DIR/.instance-icon.png.tmp`,失败时清理。
|
||||||
|
- 默认 `CONFIG_DIR` 为 `APP_DIR/config`;`.gitignore` 精确忽略这两个文件。
|
||||||
|
- 如果部署设置 `CC_WEB_CONFIG_DIR` 到仓库外,图标自动跟随外置配置目录。
|
||||||
|
- 不覆盖任何 `public/*.png`,源码升级只更新默认兜底图。
|
||||||
|
|
||||||
|
## HTTP 契约
|
||||||
|
|
||||||
|
### GET /api/instance-icon/config
|
||||||
|
|
||||||
|
公开返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"custom": true,
|
||||||
|
"version": "sha256-short-hash",
|
||||||
|
"updatedAt": "ISO-8601",
|
||||||
|
"url": "/api/instance-icon?v=sha256-short-hash"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
未配置时 `custom=false`,`version=default`,URL 仍指向统一读取接口。
|
||||||
|
|
||||||
|
### GET /api/instance-icon
|
||||||
|
|
||||||
|
- 自定义存在且有效:返回该 PNG。
|
||||||
|
- 未配置或运行文件失效:返回 `public/icon-192.png`。
|
||||||
|
- `Content-Type: image/png`、`X-Content-Type-Options: nosniff`。
|
||||||
|
- 使用 ETag/no-cache;版本化 URL 负责同页缓存刷新。
|
||||||
|
|
||||||
|
### POST /api/instance-icon
|
||||||
|
|
||||||
|
- 必须通过现有 Bearer token 鉴权。
|
||||||
|
- 仅 `Content-Type: image/png`,body 非空且不超过 4 MiB。
|
||||||
|
- 必须是标准 PNG 签名,IHDR 宽高均为 512。
|
||||||
|
- 固定路径原子写入;返回最新配置对象。
|
||||||
|
|
||||||
|
### DELETE /api/instance-icon
|
||||||
|
|
||||||
|
- 必须通过现有 Bearer token 鉴权。
|
||||||
|
- 删除实例运行文件,幂等返回默认配置对象。
|
||||||
|
|
||||||
|
### GET /api/site.webmanifest
|
||||||
|
|
||||||
|
- 未自定义:保留现有 192/512 默认图标声明。
|
||||||
|
- 已自定义:使用版本化 `/api/instance-icon`,声明 512×512。
|
||||||
|
|
||||||
|
## 前端契约
|
||||||
|
|
||||||
|
- head 中 favicon、Apple Touch 与 Manifest 从首屏即指向动态 API,默认服务端兜底避免闪烁。
|
||||||
|
- 需要动态更新的图片用 `data-instance-icon` 标识;图标 link 用 `data-instance-icon-link` 标识。
|
||||||
|
- `instanceIconUrl(config)` 是唯一 URL 生成入口;浏览器通知和 Service Worker 默认也用 `/api/instance-icon`。
|
||||||
|
- 外观设置中新增“实例图标”区:预览、选择图标、恢复默认、状态文字。
|
||||||
|
- 客户端拒绝非 PNG/JPEG/WebP;使用 canvas 居中裁剪,不拉伸;上传失败保留旧图标。
|
||||||
|
|
||||||
|
## 错误语义
|
||||||
|
|
||||||
|
- 401:未鉴权写操作。
|
||||||
|
- 400:空内容、错误 MIME、非法 PNG、非 512×512。
|
||||||
|
- 413:超过 4 MiB。
|
||||||
|
- 500:原子保存/读取不可恢复错误;前端显示服务端中文消息。
|
||||||
|
|
||||||
|
## 测试顺序
|
||||||
|
|
||||||
|
1. 增加聚焦服务集成测试并确认在实现前失败。
|
||||||
|
2. 覆盖默认读取、鉴权、非法输入、上传、覆盖版本、恢复默认与 Git 外置路径。
|
||||||
|
3. 增加前端静态/DOM 契约测试,覆盖设置 UI 和所有图标消费点。
|
||||||
|
4. 实现最小代码使测试通过,再运行总回归。
|
||||||
40
.trellis/tasks/08-25-instance-custom-icon/prd.md
Normal file
40
.trellis/tasks/08-25-instance-custom-icon/prd.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 实例自定义图标需求
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
用户在多台机器部署 cc-web。当前各实例使用相同图标,浏览器和 cc-web 界面中难以快速区分,需要每个实例可独立设置图标。
|
||||||
|
|
||||||
|
## 用户故事
|
||||||
|
|
||||||
|
作为 cc-web 管理者,我希望在设置页从本机选择实例图标,以便多实例并行使用时能直观看出当前机器;升级源码后,该选择仍应保留。
|
||||||
|
|
||||||
|
## 功能要求
|
||||||
|
|
||||||
|
1. 设置页新增实例图标设置项,展示当前图标。
|
||||||
|
2. 用户可选择受支持的本地图片并保存,保存成功后当前页面使用新图标。
|
||||||
|
3. 用户可恢复默认图标。
|
||||||
|
4. 未配置自定义图标的安装保持当前默认外观。
|
||||||
|
5. 自定义图标文件及选择状态存储在 Git 管理之外,`git pull` 不覆盖。
|
||||||
|
6. 服务端校验文件类型与大小,拒绝非法或超限输入并返回明确错误。
|
||||||
|
7. 图标更新后避免浏览器继续显示旧缓存。
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
- 不实现在线图标市场、URL 抓取、裁剪编辑器或多套图标历史。
|
||||||
|
- 不改变现有主题系统和品牌名称。
|
||||||
|
- 不自动修改操作系统桌面快捷方式或已安装 PWA 的原生图标。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 默认、上传、刷新后持久化、覆盖更新、恢复默认、非法类型和超限文件均有自动化覆盖。
|
||||||
|
- 侧栏/品牌入口与浏览器页签等现有运行时图标消费点按实际架构统一接入。
|
||||||
|
- 自定义图标数据未出现在 `git status` 的已跟踪/未跟踪源码改动中。
|
||||||
|
- 相关测试与构建通过,未破坏既有设置保存行为。
|
||||||
|
|
||||||
|
## 技术约束
|
||||||
|
|
||||||
|
- 浏览器接受 PNG/JPEG/WebP 选择,居中裁剪为 512×512 PNG 后上传;服务端最大接收 4 MiB。
|
||||||
|
- 自定义图片固定写入 `CC_WEB_CONFIG_DIR/instance-icon.png`(未设置环境变量时为 `config/instance-icon.png`),通过原子替换保存。
|
||||||
|
- 公开读取使用 `/api/instance-icon`;写入和恢复默认必须 Bearer 鉴权。
|
||||||
|
- 图标内容哈希作为版本,设置成功后更新 favicon、登录 Logo、通知图标和动态 Manifest。
|
||||||
|
- 默认行为继续使用现有源码图标;不保证操作系统对已安装 PWA 图标的即时刷新。
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 服务端设置与外置存储调研
|
||||||
|
|
||||||
|
## 架构与持久化
|
||||||
|
|
||||||
|
- `server.js` 使用 Node 原生 HTTP 与 `ws`,路由集中在 HTTP 回调和 WebSocket 消息 switch。
|
||||||
|
- `CONFIG_DIR = CC_WEB_CONFIG_DIR || APP_DIR/config`,现有运行配置均从该目录读写;测试会把它指向临时目录。
|
||||||
|
- 默认 `config/` 位于仓库内,但 `.gitignore` 对运行文件逐项忽略;实例图标必须新增精确规则,生产还可通过 `CC_WEB_CONFIG_DIR` 完全外置。
|
||||||
|
|
||||||
|
## 图片上传先例
|
||||||
|
|
||||||
|
- `POST /api/attachments` 已采用原始二进制 body,不使用 multipart;通过 Bearer token 鉴权。
|
||||||
|
- 既有逻辑包含大小上限、MIME 白名单、空内容拒绝、固定/清理后的文件名与读取鉴权。
|
||||||
|
- 实例图标上传可沿用“二进制 REST + 鉴权”,不应把大图片转 base64 塞入 WebSocket 配置消息。
|
||||||
|
|
||||||
|
## 静态响应与安全
|
||||||
|
|
||||||
|
- 现有静态资源响应使用 `no-store`,并检查解析后的路径仍位于 `PUBLIC_DIR`。
|
||||||
|
- 实例图标应只使用服务端固定文件名,写入临时文件后原子 rename;不接受客户端路径。
|
||||||
|
- 推荐只保存规范化 PNG,服务端检查 PNG 签名与 IHDR 尺寸,避免依赖声明 MIME。
|
||||||
|
|
||||||
|
## 推荐接口
|
||||||
|
|
||||||
|
- `GET /api/instance-icon/config`:返回是否自定义、版本、更新时间和图标 URL;不含敏感信息。
|
||||||
|
- `GET /api/instance-icon`:公开返回自定义 PNG;未配置时返回默认 `public/icon-192.png`。
|
||||||
|
- `POST /api/instance-icon`:Bearer 鉴权,接收固定尺寸 PNG,校验后原子写入。
|
||||||
|
- `DELETE /api/instance-icon`:Bearer 鉴权,恢复默认图标。
|
||||||
|
- `GET /api/site.webmanifest`:根据是否自定义返回默认或实例图标声明。
|
||||||
|
|
||||||
|
## 验证重点
|
||||||
|
|
||||||
|
- 默认回退、未鉴权写入、非法 MIME/签名/尺寸、超限、覆盖更新、恢复默认。
|
||||||
|
- 运行文件位于 `CONFIG_DIR`,不进入 `PUBLIC_DIR` 且被 Git 忽略。
|
||||||
|
- 自定义 URL 带内容版本,避免同页更新仍显示旧图。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 前端图标与设置链路调研
|
||||||
|
|
||||||
|
## 图标消费点
|
||||||
|
|
||||||
|
- `public/index.html`:favicon、32px favicon、Apple Touch 图标、Manifest,以及登录页 `icon-192.png`。
|
||||||
|
- `public/app.js`:浏览器通知固定使用 `/icon-192.png`,强制修改密码面板再次渲染登录 Logo。
|
||||||
|
- `public/sw.js`:Service Worker 通知默认使用 `/icon-192.png`。
|
||||||
|
- `public/site.webmanifest`:PWA 安装图标使用 `icon-192.png` 与 `icon-512.png`。
|
||||||
|
|
||||||
|
## 设置链路
|
||||||
|
|
||||||
|
- `#settings-btn` 调用 `showSettingsPanel()`,Codex 与 Claude 设置页都复用 `buildAppearanceSettingsHtml()` / `mountAppearanceSettings()`。
|
||||||
|
- 设置读取与保存统一走 WebSocket 消息;服务端在消息 switch 中分发 `get_*_config` / `save_*_config` 并回推配置。
|
||||||
|
- 新功能适合增加独立的实例图标配置消息,避免混入 Codex/Claude 模型配置。
|
||||||
|
|
||||||
|
## 静态资源与缓存
|
||||||
|
|
||||||
|
- 静态资源由 `server.js` 从 `PUBLIC_DIR` 返回,响应使用 `Cache-Control: no-store, max-age=0`。
|
||||||
|
- 默认图标未参与前端资产 hash;自定义运行时 URL 应带版本参数或 ETag,确保同页更新。
|
||||||
|
- PWA/操作系统已安装图标受平台缓存控制,即使动态 Manifest 更新也不保证立即刷新;浏览器页签、登录 Logo 与通知图标可可靠动态更新。
|
||||||
|
|
||||||
|
## 测试与风险
|
||||||
|
|
||||||
|
- 总回归入口是 `npm run regression`;`scripts/regression.js` 已覆盖设置页消息与静态结构。
|
||||||
|
- 必须覆盖默认回退、设置页上传/恢复消息、通知图标与登录 Logo 使用统一 URL。
|
||||||
|
- 不应修改主题级设置按钮图标;实例品牌图标与主题装饰资产是不同职责。
|
||||||
26
.trellis/tasks/08-25-instance-custom-icon/task.json
Normal file
26
.trellis/tasks/08-25-instance-custom-icon/task.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "instance-custom-icon",
|
||||||
|
"name": "instance-custom-icon",
|
||||||
|
"title": "实例自定义图标",
|
||||||
|
"description": "",
|
||||||
|
"status": "in_progress",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "shiyue",
|
||||||
|
"assignee": "shiyue",
|
||||||
|
"createdAt": "2026-08-25",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{"file":".trellis/spec/frontend/index.md","reason":"前端任务入口与规范索引"}
|
||||||
|
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"验证前端质量与回归测试覆盖"}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{"file":".trellis/spec/frontend/index.md","reason":"前端任务入口与规范索引"}
|
||||||
|
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"前端质量与回归测试要求"}
|
||||||
|
{"file":".trellis/spec/frontend/component-guidelines.md","reason":"侧栏控件结构与无障碍约束参考"}
|
||||||
|
{"file":".trellis/spec/frontend/state-management.md","reason":"本地持久化显示状态的状态管理参考"}
|
||||||
38
.trellis/tasks/08-26-mcp-session-visibility-toggle/prd.md
Normal file
38
.trellis/tasks/08-26-mcp-session-visibility-toggle/prd.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# MCP 会话显示隐藏开关
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
侧栏会展示大量由 MCP 创建的协作会话。用户通常不需要逐条查看这些会话,希望通过一个紧凑按钮减少列表噪声,同时保留普通会话和仍需关注的运行态会话。
|
||||||
|
|
||||||
|
## 用户故事
|
||||||
|
|
||||||
|
作为经常使用 MCP 协作能力的用户,我希望用一个图标快速显示或隐藏 MCP 创建的会话,使侧栏保持简洁,并且不会丢失当前会话或仍在运行的任务入口。
|
||||||
|
|
||||||
|
## 功能要求
|
||||||
|
|
||||||
|
1. 在侧栏搜索框右侧、与高级搜索按钮并排增加一个图标按钮。
|
||||||
|
2. 按钮仅控制 `createdFromKind === 'mcp'` 的会话;正常会话始终显示。
|
||||||
|
3. 隐藏模式仍显示当前打开或 `isRunning === true` 的 MCP 会话。
|
||||||
|
4. 其他 MCP 会话在隐藏模式下不参与置顶区、项目分组和未分组列表渲染。
|
||||||
|
5. 输入会话搜索关键词时,全部 MCP 会话恢复参与匹配;清空搜索后重新应用隐藏规则。
|
||||||
|
6. 首次且没有保存偏好时默认显示 MCP 会话。
|
||||||
|
7. 用户切换结果保存于当前浏览器,刷新或下次打开继续沿用。
|
||||||
|
8. 控件仅显示一个状态图标,不显示文字或数量角标;通过悬停提示与无障碍属性表达动作和状态。
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
- 不删除、归档或修改任何会话数据。
|
||||||
|
- 不改变服务端会话列表接口、排序协议或 MCP 创建流程。
|
||||||
|
- 不改变项目折叠、置顶和旧会话“加载更多”的既有语义。
|
||||||
|
- 不增加设置页配置项。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 默认状态与当前行为一致:全部会话正常显示。
|
||||||
|
- 切换隐藏后,普通会话、当前 MCP 会话和运行中 MCP 会话仍显示,其余 MCP 会话消失。
|
||||||
|
- 置顶 MCP 会话在非当前、非运行时同样隐藏。
|
||||||
|
- 隐藏状态下搜索可找到匹配的 MCP 会话;清空搜索后再次隐藏。
|
||||||
|
- 刷新页面后保留最后一次切换状态。
|
||||||
|
- 图标按钮在主要主题和窄屏侧栏中不挤压或遮挡现有控件。
|
||||||
|
- 新增回归测试覆盖过滤、持久化、搜索覆盖、状态例外与 DOM/无障碍契约。
|
||||||
|
|
||||||
26
.trellis/tasks/08-26-mcp-session-visibility-toggle/task.json
Normal file
26
.trellis/tasks/08-26-mcp-session-visibility-toggle/task.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "mcp-session-visibility-toggle",
|
||||||
|
"name": "mcp-session-visibility-toggle",
|
||||||
|
"title": "MCP 会话显示隐藏开关",
|
||||||
|
"description": "",
|
||||||
|
"status": "in_progress",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "shiyue",
|
||||||
|
"assignee": "shiyue",
|
||||||
|
"createdAt": "2026-08-26",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"后端质量约定"}
|
||||||
|
{"file":".planning/create-conversation-async/findings.md","reason":"工具契约与回传链路证据"}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"后端质量约定"}
|
||||||
|
{"file":".planning/create-conversation-async/findings.md","reason":"工具契约与回传链路证据"}
|
||||||
10
.trellis/tasks/09-16-create-conversation-async/prd.md
Normal file
10
.trellis/tasks/09-16-create-conversation-async/prd.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 修复创建会话异步等待提示
|
||||||
|
目标:按用户最新要求,只修正 ccweb_create_conversation 提示词,明确“启用 requestReply 后,由系统异步回传结果;除非用户明确要求轮询,否则不主动轮询等待”。
|
||||||
|
1. 核验工具提示词和异步回复链路 — completed
|
||||||
|
2. 修正创建会话的异步交互契约 — completed
|
||||||
|
3. 补充并运行相关回归验证 — completed
|
||||||
|
4. 核对生效条件并整理交付 — completed
|
||||||
|
边界:只改 lib/ccweb-mcp-server.js 的 description 和 requestReply description;不改功能、不改默认值、不扩展返回值、不新增测试、不重启服务。
|
||||||
|
验收:description 保留用户明确指定轮询的例外;false 表示不自动回传,true 表示完成后回传并触发来源继续运行;两者创建调用均立即返回;true 必须携带 initialMessage。使用现有单测、语法检查与导出提示词检查验证。
|
||||||
|
工具限制:当前没有 update_plan 工具,以同文案文件计划与 CSV 同步状态。
|
||||||
|
错误记录:早期工具调用有格式与未打印输出问题,已改为 await tools + text;没有据此变更代码。用户明确缩小范围后取消任何绝对禁止轮询要求。
|
||||||
26
.trellis/tasks/09-16-create-conversation-async/task.json
Normal file
26
.trellis/tasks/09-16-create-conversation-async/task.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "create-conversation-async",
|
||||||
|
"name": "create-conversation-async",
|
||||||
|
"title": "修复创建会话异步等待提示",
|
||||||
|
"description": "",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "shiyue",
|
||||||
|
"assignee": "shiyue",
|
||||||
|
"createdAt": "2026-09-16",
|
||||||
|
"completedAt": "2026-09-17",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
6
Gitea MCP 失败链路收尾 TO DO list.csv
Normal file
6
Gitea MCP 失败链路收尾 TO DO list.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id,item,status,done_at,notes
|
||||||
|
1,核对当前 Gitea runner、Codex App 启动和回执结算链路,DONE,2026-08-24T16:09:10+08:00,已核对 server.js runner、Codex App thread/start 和回执结算路径
|
||||||
|
2,增加 gitea-mcp stdio 预检与有限超时错误分类,DONE,2026-08-24T16:15:24+08:00,已接入 stdio initialize 预检、稳定错误码和测试 mock
|
||||||
|
3,增加针对缺失、启动失败、握手失败的独立回归断言,并验证每类只发一次 giteabot 失败回执,DONE,2026-08-24T16:16:08+08:00,独立预检单测覆盖四类失败、线程注入和失败回执守门;全部通过
|
||||||
|
4,同步中文文档和运行配置说明,明确线程级注入与不自动安装约束,DONE,2026-08-24T16:16:08+08:00,已同步 ARCHITECTURE/CODEX-INTEGRATION/DEPLOYMENT/TESTING 与 .env.example
|
||||||
|
5,运行静态检查、单测、Webhook 回归并核对运行状态,IN_PROGRESS,,
|
||||||
|
6
Gitea Workflow 管理页收缩整改 TO DO list.csv
Normal file
6
Gitea Workflow 管理页收缩整改 TO DO list.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id,item,status,done_at,notes
|
||||||
|
1,核对后端配置提交兼容性与当前页面字段,DONE,2026-08-24T09:59:38+08:00,已完成控制区/配置区收缩,开始做验证
|
||||||
|
2,收缩 gitea-workflow.js 配置与控制区,IN_PROGRESS,,
|
||||||
|
3,同步 gitea-workflow.css 以移除多余输入样式,TODO,,
|
||||||
|
4,运行语法与专项回归检查,TODO,,
|
||||||
|
5,真实浏览器验证 washi 和 carbon 双视口,TODO,,
|
||||||
|
27
README.md
27
README.md
@@ -173,6 +173,8 @@ cd cc-web
|
|||||||
- 自动安装 PM2
|
- 自动安装 PM2
|
||||||
- 使用 `npm ci` 或 `npm install` 安装依赖
|
- 使用 `npm ci` 或 `npm install` 安装依赖
|
||||||
- 以 `ccweb` 为默认 PM2 应用名启动或重启服务
|
- 以 `ccweb` 为默认 PM2 应用名启动或重启服务
|
||||||
|
- 启动前自动释放配置端口(默认 `8002`)
|
||||||
|
- 在 CentOS 7 上如果源码方式启动失败,自动回退到 `dist-exe/bun-linux-x64-baseline/cc-web`
|
||||||
|
|
||||||
临时前台运行:
|
临时前台运行:
|
||||||
|
|
||||||
@@ -211,9 +213,15 @@ dist-exe/bun-linux-x64-baseline/
|
|||||||
```bash
|
```bash
|
||||||
cd /opt/cc-web
|
cd /opt/cc-web
|
||||||
chmod +x cc-web
|
chmod +x cc-web
|
||||||
PORT=8002 CC_WEB_PASSWORD='请改成强密码' ./cc-web
|
PORT=8002 ./cc-web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如果项目根目录同时包含 `start.sh` 和 `dist-exe/`,直接执行 `./start.sh` 即可。
|
||||||
|
脚本默认先按原有 Node/PM2 方式启动;仅当源码方式启动失败且系统识别为 CentOS 7
|
||||||
|
时,才自动切换到 `dist-exe/bun-linux-x64-baseline/cc-web`。启动前会先释放监听端口,
|
||||||
|
默认端口为 `8002`。已有 `config/auth.json` 时会直接复用,不会强制要求重新设置
|
||||||
|
`CC_WEB_PASSWORD`。
|
||||||
|
|
||||||
这个发布包只包含 cc-web 服务本体和前端资源,**不会把 Claude/Codex CLI 打进包里**。
|
这个发布包只包含 cc-web 服务本体和前端资源,**不会把 Claude/Codex CLI 打进包里**。
|
||||||
运行时仍调用宿主机上的 CLI:
|
运行时仍调用宿主机上的 CLI:
|
||||||
|
|
||||||
@@ -254,6 +262,10 @@ node server.js
|
|||||||
| `CC_WEB_LOGS_DIR` | `./logs` | 日志目录覆写,常用于测试隔离 |
|
| `CC_WEB_LOGS_DIR` | `./logs` | 日志目录覆写,常用于测试隔离 |
|
||||||
| `CC_WEB_CODEX_APP_WORKER` | 开启 | 设为 `0` / `false` / `off` 可关闭 Codex App worker |
|
| `CC_WEB_CODEX_APP_WORKER` | 开启 | 设为 `0` / `false` / `off` 可关闭 Codex App worker |
|
||||||
| `CC_WEB_PROCESS_CLEAN_PATH` | 自动探测 | 清理旧 Codex app-server 进程时使用的匹配路径覆写 |
|
| `CC_WEB_PROCESS_CLEAN_PATH` | 自动探测 | 清理旧 Codex app-server 进程时使用的匹配路径覆写 |
|
||||||
|
| `CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC` | `30` | Codex App MCP 启动等待秒数,范围 10-300 |
|
||||||
|
| `CC_WEB_CODEX_MCP_TOOL_TIMEOUT_SEC` | `60` | ccweb MCP 工具调用超时秒数,范围 10-600 |
|
||||||
|
| `CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS` | `35000` | 重载 MCP 等待最终状态的毫秒数,范围 1000-180000 |
|
||||||
|
| `CC_WEB_CODEX_APP_MCP_RELOAD_TRACK_MS` | `60000` 或等待窗口+10秒的较大值 | 重载状态关联保留时间,范围 5000-300000 |
|
||||||
|
|
||||||
还有若干面向大历史、长输出和 Codex App 状态落盘的高级限制参数,例如:
|
还有若干面向大历史、长输出和 Codex App 状态落盘的高级限制参数,例如:
|
||||||
|
|
||||||
@@ -274,7 +286,7 @@ node server.js
|
|||||||
| `config/auth.json` | 登录密码配置,运行时生成 |
|
| `config/auth.json` | 登录密码配置,运行时生成 |
|
||||||
| `config/notify.json` | 通知渠道配置,运行时生成 |
|
| `config/notify.json` | 通知渠道配置,运行时生成 |
|
||||||
| `config/codex.json` | Codex 默认模型等配置,运行时生成 |
|
| `config/codex.json` | Codex 默认模型等配置,运行时生成 |
|
||||||
| `config/cross-conversation-replies.json` | 跨对话等待回复状态 |
|
| `config/cross-conversation-replies.json` | 跨对话等待回复状态(运行时生成,已忽略版本管理) |
|
||||||
| `sessions/*.json` | ccweb 会话历史 |
|
| `sessions/*.json` | ccweb 会话历史 |
|
||||||
| `sessions/{id}-run/` | 单次运行输出、PID、Codex App 状态 |
|
| `sessions/{id}-run/` | 单次运行输出、PID、Codex App 状态 |
|
||||||
| `sessions/_attachments/` | 图片附件与元数据 |
|
| `sessions/_attachments/` | 图片附件与元数据 |
|
||||||
@@ -439,6 +451,17 @@ WantedBy=multi-user.target
|
|||||||
- 对公网开放时建议放到 HTTPS 反向代理后,并使用强密码
|
- 对公网开放时建议放到 HTTPS 反向代理后,并使用强密码
|
||||||
|
|
||||||
|
|
||||||
|
# Gitea Workflow
|
||||||
|
|
||||||
|
cc-web 已支持在 Gitea Issue/PR 普通评论中 `@ccweb-bot` 触发 Codex App 工作流。
|
||||||
|
完整需求、部署、配置和验收说明见 [`docs/gitea-workflow/PRD.md`](docs/gitea-workflow/PRD.md)、
|
||||||
|
[`docs/gitea-workflow/DEPLOYMENT.md`](docs/gitea-workflow/DEPLOYMENT.md)。
|
||||||
|
|
||||||
|
最小配置:设置 `CC_WEB_GITEA_HOST`、`CC_WEB_GITEA_BOT_TOKEN`、
|
||||||
|
`CC_WEB_GITEA_WEBHOOK_SECRET` 和 `CC_WEB_GITEA_WORKSPACE_ROOT`,在 Gitea 创建指向
|
||||||
|
`POST /api/gitea/webhook` 的 Webhook(启用 Secret),然后安装官方 `gitea-mcp`。
|
||||||
|
管理页可查看任务/仓库/审计并执行暂停、停用、取消和中止。
|
||||||
|
|
||||||
## 其他
|
## 其他
|
||||||
|
|
||||||
本项目源自 cc-web,并在实际使用中围绕 Codex App 与 ccweb MCP 做了重构和扩展。
|
本项目源自 cc-web,并在实际使用中围绕 Codex App 与 ccweb MCP 做了重构和扩展。
|
||||||
|
|||||||
BIN
bin/gitea-mcp
Executable file
BIN
bin/gitea-mcp
Executable file
Binary file not shown.
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"updatedAt": "2026-08-20T15:45:04.108Z",
|
|
||||||
"replies": []
|
|
||||||
}
|
|
||||||
Binary file not shown.
197
docs/WEBSOCKET-TRANSPORT-EVALUATION.md
Normal file
197
docs/WEBSOCKET-TRANSPORT-EVALUATION.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# cc-web WebSocket 替换为 SSE / WebTransport 评估报告
|
||||||
|
|
||||||
|
日期:2026-09-14
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
当前项目不适合直接把 WebSocket 整体替换成 SSE 或 WebTransport。
|
||||||
|
|
||||||
|
推荐保留 WebSocket 作为默认双向控制通道,同时增加“认证后的 SSE 下行通道 + HTTP
|
||||||
|
命令接口”的渐进式方案。这样可以先降低代理对 WebSocket 的依赖,并保留现有协议和
|
||||||
|
回退能力。WebTransport 适合后续独立 PoC,不建议作为近期生产替换目标。
|
||||||
|
|
||||||
|
| 方案 | 当前项目可行性 | 适合的范围 | 主要结论 |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| 继续使用 WebSocket | 高 | 现有全部功能 | 成本最低,协议已覆盖双向消息、审批和实时输出 |
|
||||||
|
| SSE + HTTP 命令 | 高(渐进式) | 服务端事件下行、流式输出 | 推荐;需要事件总线、重放和认证改造 |
|
||||||
|
| 纯 SSE 替换 | 中低 | 只读监控或单向推送 | 不匹配当前大量客户端命令和交互式请求 |
|
||||||
|
| WebTransport | 中低(近期),高(专用场景) | 高并发低延迟、可靠流+不可靠数据报 | 基础设施和 Node 服务端生态成本过高 |
|
||||||
|
|
||||||
|
## 当前 WebSocket 的职责
|
||||||
|
|
||||||
|
项目的 WebSocket 并非只用来传输模型文本,而是整个浏览器会话的双向 RPC 通道。
|
||||||
|
|
||||||
|
- `server.js:9330-9575` 创建 `/ws` 的 `WebSocketServer`,在首帧完成密码或 token
|
||||||
|
认证,然后按 JSON `type` 分发命令。
|
||||||
|
- 客户端在 `public/app.js:7781-7870` 建立连接、解析 JSON、指数退避重连,并在
|
||||||
|
`public/app.js:7191-7204` 等位置通过同一连接发送命令。
|
||||||
|
- 普通 Claude/Codex 运行时在 `lib/agent-runtime.js:359-590` 产生
|
||||||
|
`text_delta`、`content_blocks`、`tool_start/update/end`、`usage` 等事件;Codex
|
||||||
|
App 在 `lib/codex-app-runtime.js:437` 复用相同下行抽象。
|
||||||
|
- 服务端还按会话查看关系发送事件(`server.js:6835-6851`),并向所有认证客户端
|
||||||
|
广播任务看板和后台完成事件(`server.js:6512-6529`、`server.js:6827-6831`)。
|
||||||
|
- `activeProcesses`、`activeCodexAppTurns` 和 `wsSessionMap`
|
||||||
|
(`server.js:1420-1503`)把运行中的进程、当前会话和连接绑定在内存中。断线时
|
||||||
|
`handleDisconnect`(`server.js:11397-11425`)解绑连接,但进程继续运行,重连后再
|
||||||
|
恢复查看。
|
||||||
|
- 客户端和服务端都有心跳:服务端 WebSocket ping 在 `server.js:9578-9604`,客户端
|
||||||
|
应用层 heartbeat 在 `public/app.js:7720-7775`。这也是当前反向代理长连接稳定性的
|
||||||
|
一部分。
|
||||||
|
|
||||||
|
因此,替换传输层必须保留以下语义:实时增量、工具调用生命周期、审批/引导输入的
|
||||||
|
双向往返、会话切换与恢复、跨会话广播、断线重连、重复命令保护和后台任务通知。
|
||||||
|
|
||||||
|
## SSE 评估
|
||||||
|
|
||||||
|
### 可行性
|
||||||
|
|
||||||
|
SSE 很适合承载本项目的服务端下行事件。`text_delta`、工具状态、`done`、会话列表、
|
||||||
|
任务看板和提示事件都可以编码为带 `event`、`id`、`data` 的 SSE 帧。浏览器原生
|
||||||
|
`EventSource` 自带自动重连,服务端可用注释心跳保持连接。
|
||||||
|
|
||||||
|
但 SSE 只能由服务器向浏览器推送。当前客户端发送的 `message`、`abort`、会话管理、
|
||||||
|
设置、任务看板查询、审批响应和引导输入响应必须迁移到 HTTP `POST`/`PATCH`/`DELETE`
|
||||||
|
命令接口,或继续由 WebSocket 承担。这意味着“纯 SSE”不是小改动;“SSE 下行 + HTTP
|
||||||
|
命令”才是可行的替代架构。
|
||||||
|
|
||||||
|
### 优点
|
||||||
|
|
||||||
|
- 基于普通 HTTP,Nginx、Caddy、云负载均衡和审计工具更容易接入。
|
||||||
|
- 浏览器 API 简单,断线重连和 `Last-Event-ID` 已有标准语义。
|
||||||
|
- 事件天然是文本 JSON,与当前 `wsSend(JSON.stringify(data))` 的消息模型接近。
|
||||||
|
- 单向输出的代码边界清晰,适合模型流式文本、工具进度和只读监控。
|
||||||
|
- 不需要 UDP/443、QUIC、HTTP/3 或新的服务端运行时。
|
||||||
|
|
||||||
|
### 缺点和改造点
|
||||||
|
|
||||||
|
- 不能承载现有客户端到服务端的命令;需要新建命令路由、请求 ID、错误响应和幂等控制。
|
||||||
|
- 原生 `EventSource` 不能设置自定义 `Authorization` 头。当前 token 放在首个 WebSocket
|
||||||
|
JSON 帧中,迁移时应优先改为安全 Cookie(配套 CSRF 防护),或使用 `fetch` 流式读取;
|
||||||
|
把 token 放 URL 会进入代理日志、历史记录和监控标签,不建议。
|
||||||
|
- 当前仅保存在会话 JSON 和运行态文件中的状态不足以重放每个增量。必须为 SSE 事件分配
|
||||||
|
单调 `id`,并增加短期事件缓冲或按 `sessionId` 重发快照,否则网络抖动时会丢字、丢工具状态。
|
||||||
|
- HTTP/1.1 下浏览器对同源并发连接数有限;HTTP/2 可改善连接复用,但反向代理必须关闭
|
||||||
|
响应缓冲并提高读超时。Nginx 通常需要 `proxy_buffering off`、`X-Accel-Buffering: no`
|
||||||
|
和足够大的 `proxy_read_timeout`。
|
||||||
|
- 一条 SSE 流是有序可靠字节流;慢客户端会形成反压,需要限制队列、丢弃可重建事件或
|
||||||
|
断开慢连接,不能无限堆积内存。
|
||||||
|
- 水平扩展时,SSE 客户端和运行进程仍绑定单个 Node 实例;需要粘性会话或 Redis/NATS
|
||||||
|
等发布订阅与事件重放层。当前项目没有这层基础设施。
|
||||||
|
|
||||||
|
### 对当前项目的评分
|
||||||
|
|
||||||
|
| 指标 | SSE 下行 + HTTP 命令 |
|
||||||
|
|---|---:|
|
||||||
|
| 代码复用 | 7/10 |
|
||||||
|
| 浏览器兼容 | 9/10 |
|
||||||
|
| 代理/部署 | 8/10 |
|
||||||
|
| 双向交互适配 | 6/10 |
|
||||||
|
| 断线恢复 | 需新增 6/10 |
|
||||||
|
| 近期落地建议 | 推荐灰度 |
|
||||||
|
|
||||||
|
## WebTransport 评估
|
||||||
|
|
||||||
|
### 可行性
|
||||||
|
|
||||||
|
WebTransport 基于 HTTP/3/QUIC,同时提供可靠的双向流和可丢失的数据报,理论上可以
|
||||||
|
较完整地承接当前 WebSocket 的双向 JSON 协议。普通命令、审批和模型文本应使用可靠
|
||||||
|
双向流;只有明确允许丢失的高频状态才考虑数据报。
|
||||||
|
|
||||||
|
当前项目的 Node `http.createServer` + `ws` 结构没有现成 WebTransport 入口。引入后需要
|
||||||
|
HTTP/3/QUIC 服务端库或独立网关、证书和连接管理;现有 `/ws` 的升级、心跳、认证、
|
||||||
|
反向代理配置和运维监控均不能直接复用。
|
||||||
|
|
||||||
|
### 优点
|
||||||
|
|
||||||
|
- 原生双向通信,命令和事件不必拆成两套协议。
|
||||||
|
- QUIC 在多条流之间避免 TCP 层队头阻塞;建立连接和网络切换体验可能更好。
|
||||||
|
- 可按场景选择可靠流或低延迟数据报,适合未来高频协作光标、实时遥测等功能。
|
||||||
|
- 连接由 HTTP/3 承载,具备现代传输层的多路复用能力。
|
||||||
|
|
||||||
|
### 缺点和风险
|
||||||
|
|
||||||
|
- 浏览器必须处于安全上下文,服务端和代理必须支持 HTTP/3/QUIC;UDP/443、防火墙、
|
||||||
|
云负载均衡和企业网络放行都成为部署前置条件。
|
||||||
|
- Node 核心当前没有与 `ws` 同等成熟、可直接替换的稳定高层 WebTransport 服务端 API,
|
||||||
|
需要评估第三方库或独立网关的维护状态、内存安全和协议兼容性。
|
||||||
|
- 现有 Nginx/HTTP 反向代理配置按 HTTP/1.1 WebSocket 编写,不能假设能透明转发
|
||||||
|
WebTransport;需要逐个验证 HTTP/3 终止点、QUIC 到后端的转发方式和超时策略。
|
||||||
|
- 数据报不保证送达、顺序或不重复,不能承载文本增量、审批、abort、会话切换等关键
|
||||||
|
消息;可靠流仍需实现应用层消息边界、背压、重连和幂等。
|
||||||
|
- 连接迁移、连接 ID、TLS、HTTP/3 日志和指标与当前 WebSocket 运维经验不同,故障排查
|
||||||
|
成本明显更高。
|
||||||
|
- Safari、旧版浏览器、企业代理和受限网络的可用性需要实测,生产仍需 WebSocket/SSE
|
||||||
|
回退;这会带来三套客户端和协议测试矩阵。
|
||||||
|
|
||||||
|
### 对当前项目的评分
|
||||||
|
|
||||||
|
| 指标 | WebTransport |
|
||||||
|
|---|---:|
|
||||||
|
| 代码复用 | 5/10 |
|
||||||
|
| 浏览器兼容 | 5/10(需目标用户实测) |
|
||||||
|
| 代理/部署 | 3/10 |
|
||||||
|
| 双向交互适配 | 8/10 |
|
||||||
|
| 低延迟/多路复用潜力 | 9/10 |
|
||||||
|
| 近期落地建议 | 不推荐直接替换 |
|
||||||
|
|
||||||
|
## 改造规模与风险
|
||||||
|
|
||||||
|
| 领域 | SSE 方案 | WebTransport 方案 |
|
||||||
|
|---|---|---|
|
||||||
|
| 服务端 | 抽象 `wsSend` 为事件发布器;新增 SSE 连接、命令 API、事件 ID/重放 | 替换连接层、增加 HTTP/3/QUIC 服务端和可靠流协议 |
|
||||||
|
| 前端 | `EventSource`/fetch 流读取;把 `send()` 改为 HTTP 命令;保留统一消息处理器 | 新建 WebTransport 客户端、流帧协议、能力探测和多级回退 |
|
||||||
|
| 认证 | Cookie+CSRF 或 fetch 自定义头;处理连接失效 | QUIC 握手后应用认证、连接恢复和 token 轮换 |
|
||||||
|
| 恢复 | `Last-Event-ID`、事件缓冲、会话快照 | 连接迁移、流重建、消息幂等与快照 |
|
||||||
|
| 部署 | 代理关闭缓冲、长超时,HTTP/2 优先 | HTTP/3、UDP/443、证书、网关、监控和防火墙 |
|
||||||
|
| 测试 | 事件顺序、重放、慢客户端、代理超时、CSRF | 可靠/不可靠流、丢包、网络切换、浏览器和代理矩阵 |
|
||||||
|
| 估算 | 2–4 人周做灰度骨架,4–8 人周完成替换 | 6–12 人周 PoC,生产化通常更久,取决于网关和网络 |
|
||||||
|
|
||||||
|
上述估算不包含新增 Redis/NATS、HTTP/3 网关或大规模压测;多实例部署会增加工作量。
|
||||||
|
|
||||||
|
## 推荐实施路线
|
||||||
|
|
||||||
|
1. 先把 `wsSend`、`sendSessionEventToViewers`、全局广播和运行时 `sendRuntime` 收敛到
|
||||||
|
一个传输无关的事件发布接口,统一事件名、`sessionId`、`requestId`、时间戳和递增
|
||||||
|
`eventId`。保留现有 WebSocket 适配器,确保这一步行为不变。
|
||||||
|
2. 增加受保护的 `/api/events` SSE 端点,只接入只读会话列表、任务事件和运行时下行事件。
|
||||||
|
对每个连接限制队列,发送注释心跳,并支持 `Last-Event-ID` 后按会话重发快照。
|
||||||
|
3. 以 Cookie 或 `fetch` 流读取解决认证,不把长期 token 放入 URL;命令端点使用
|
||||||
|
`requestId` 和幂等键,返回 202 后由 SSE 回传结果。先迁移 `message`、`abort`、会话
|
||||||
|
切换和审批响应,再迁移设置与任务看板命令。
|
||||||
|
4. 对比 WebSocket 与 SSE 的首字延迟、完整回合延迟、断线恢复丢事件数、慢客户端内存、
|
||||||
|
代理超时和移动网络表现。通过特性开关按用户或会话灰度,WebSocket 保留为回退。
|
||||||
|
5. 只有在所有命令均有 HTTP 等价物、事件重放和多实例路由验证通过后,才考虑下线默认
|
||||||
|
WebSocket;机器人和内部集成需单独迁移,不能只看浏览器 UI。
|
||||||
|
6. 如确有 QUIC 需求,再建立独立 WebTransport PoC,先验证目标浏览器、TLS/HTTP3 网关、
|
||||||
|
UDP 网络、可靠流重连和监控,再决定是否替代 SSE/WS。
|
||||||
|
|
||||||
|
## 最终建议
|
||||||
|
|
||||||
|
对 cc-web 当前“模型流式输出 + 交互式审批 + 会话控制 + 单 Node 进程状态绑定”的
|
||||||
|
形态,优先级应为:
|
||||||
|
|
||||||
|
1. **近期:继续 WebSocket,先做传输无关事件层和协议整理。**
|
||||||
|
2. **中期:SSE 下行 + HTTP 命令灰度,逐步减少对 WebSocket 的依赖。**
|
||||||
|
3. **长期:只有在确认 HTTP/3/QUIC 基础设施和用户浏览器覆盖后,才评估 WebTransport。**
|
||||||
|
|
||||||
|
直接替换为 SSE 会把双向协议问题转移到大量 HTTP 命令和恢复逻辑;直接替换为
|
||||||
|
WebTransport 则会同时引入协议、网关、网络和运维风险。混合渐进式路线能以较小范围验证
|
||||||
|
收益,并保留可回退路径。
|
||||||
|
|
||||||
|
## 本地依据与限制
|
||||||
|
|
||||||
|
- 依据当前工作树源码和 `home-cc-web` codebase-memory 索引(索引状态:ready,节点
|
||||||
|
8138、边 19178)完成;未修改现有源码。
|
||||||
|
- 评估假设仍是单 Node/PM2 实例、文件会话存储和现有反向代理形态;如果部署已经具备
|
||||||
|
HTTP/3 网关、共享消息总线或强制 Cookie 会话,WebTransport/SSE 的成本会下降。
|
||||||
|
- 本次只读核对发现 `ccweb` 的 PM2 进程约 21 分钟前被外部定时单元重启,重启计数为
|
||||||
|
61,环境中带有 `TRIGGER_UNIT=ccweb-restart-final-1789391174.timer`;该重启不是本次
|
||||||
|
评估触发的。
|
||||||
|
|
||||||
|
## 参考标准
|
||||||
|
|
||||||
|
- WHATWG Server-sent events:<https://html.spec.whatwg.org/multipage/server-sent-events.html>
|
||||||
|
- MDN Server-sent events:<https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events>
|
||||||
|
- RFC 9297 WebTransport:<https://www.rfc-editor.org/rfc/rfc9297.html>
|
||||||
|
- RFC 9298 WebTransport over HTTP/3:<https://www.rfc-editor.org/rfc/rfc9298.html>
|
||||||
|
- MDN WebTransport API:<https://developer.mozilla.org/en-US/docs/Web/API/WebTransport>
|
||||||
510
docs/gitea-workflow/ARCHITECTURE.md
Normal file
510
docs/gitea-workflow/ARCHITECTURE.md
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
# Gitea Webhook 驱动 ccweb 工作流架构设计
|
||||||
|
|
||||||
|
> 设计基线:[PRD.md](./PRD.md)
|
||||||
|
> 目标:定义并实现可实施、可恢复、可审计的协议与数据边界。
|
||||||
|
> MVP:单 Gitea 实例、全局 `ccweb-bot`。
|
||||||
|
|
||||||
|
## 1. 设计原则与边界
|
||||||
|
|
||||||
|
1. **事件先持久化,执行后置**:Webhook 线程只做验签、去重、规范化和入队,
|
||||||
|
不同步等待 Codex App 或 Gitea 回帖。
|
||||||
|
2. **仓库是互斥单元,资源是会话单元**:`repoKey` 决定工作区锁,
|
||||||
|
`sessionKey` 决定 Issue/PR 独立线程;二者不能混用。
|
||||||
|
3. **所有外部动作可重放且幂等**:delivery、起始回执、MCP 最终回帖和 REST
|
||||||
|
兜底均带关联键,重复请求只能得到已有结果。
|
||||||
|
4. **Gitea 是事实来源**:cc-web 的状态是工作流镜像;最终用户内容必须能在
|
||||||
|
Gitea Issue/PR 中查到,MCP 返回成功不等于回帖已落库。
|
||||||
|
5. **运行时协议贴近 Codex App**:初始化后发送 `initialized`,能力探测失败只
|
||||||
|
降级记录;使用 collaboration mode 时参数放入 `settings`,不重复传顶层字段。
|
||||||
|
6. **线程级注入来源上下文**:Gitea MCP 配置随 `thread/start.config` 注入,
|
||||||
|
不使用长驻 app-server 的进程级环境承载某个来源会话的上下文。
|
||||||
|
|
||||||
|
当前实现采用项目现有 JSON 持久化(`lib/gitea-workflow-store.js`),并由
|
||||||
|
`server.js` 挂接 HTTP 路由、`lib/gitea-workflow-*.js` 提供领域实现、`public/`
|
||||||
|
提供管理镜像。所有实现仍必须满足下文的原子写、唯一约束和恢复语义。
|
||||||
|
|
||||||
|
## 2. 逻辑拓扑
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌──────────────┐ HMAC Webhook ┌──────────────────────────┐
|
||||||
|
│ Gitea 实例 │ ─────────────────────────▶ │ Webhook Adapter │
|
||||||
|
│ Issue/PR │ │ 验签/规范化/去重/入队 │
|
||||||
|
└──────┬───────┘ └──────────┬───────────────┘
|
||||||
|
│ REST 查询/兜底 │
|
||||||
|
│ ▼
|
||||||
|
│ ┌──────────────────────┐
|
||||||
|
│ │ Workflow Store │
|
||||||
|
│ │ tasks/sessions/audit │
|
||||||
|
│ └──────────┬───────────┘
|
||||||
|
│ │
|
||||||
|
│ ┌──────────▼───────────┐
|
||||||
|
│ │ Scheduler │
|
||||||
|
│ │ repo lock │
|
||||||
|
│ └──────┬───────┬────────┘
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────▼─┐ ┌───▼─────────────┐
|
||||||
|
│ │ Workspace │ │ Session Manager │
|
||||||
|
│ │ clone/fetch │ │ thread/turn │
|
||||||
|
│ └──────┬────────┘ └───┬─────────────┘
|
||||||
|
│ │ │ JSON-RPC
|
||||||
|
│ │ ▼
|
||||||
|
│ │ ┌──────────────────┐
|
||||||
|
│ └─────▶│ Codex App Server │
|
||||||
|
│ │ codexapp + yolo │
|
||||||
|
│ └────────┬─────────┘
|
||||||
|
│ │ stdio MCP
|
||||||
|
│ ▼
|
||||||
|
└─────────────────────────────────────────────────┌───────────────┐
|
||||||
|
│ gitea-mcp │
|
||||||
|
│ 官方 stdio │
|
||||||
|
└───────────────┘
|
||||||
|
|
||||||
|
运维 API/UI ──只读查询/控制──▶ Workflow Store + Scheduler + Audit
|
||||||
|
```
|
||||||
|
|
||||||
|
组件职责:
|
||||||
|
|
||||||
|
| 组件 | 职责 | 不负责 |
|
||||||
|
|---|---|---|
|
||||||
|
| Webhook Adapter | 原始 body 验签、delivery 去重、Bot/mention 过滤、规范化 | 启动 Agent、直接执行 Git |
|
||||||
|
| Workflow Store | 任务、会话、turn、评论、仓库、控制面和审计的持久化 | 远程调用 |
|
||||||
|
| Scheduler | FIFO、每仓库 lease、恢复扫描 | 解析业务 prompt |
|
||||||
|
| Workspace Manager | 自动登记、clone/fetch、分支上下文、脏目录保护 | 修改用户代码内容 |
|
||||||
|
| Session Manager | Codex App initialize、thread/turn、事件序列、取消 | 直接向 Gitea 发最终正文 |
|
||||||
|
| MCP Bridge | 为线程启动官方 `gitea-mcp` stdio 并传递 host/token | 代替 Agent 决策 |
|
||||||
|
| Reply Verifier | 查询评论、校验 taskId/turnId/resourceKey、触发有限补偿 | 无条件重复回帖 |
|
||||||
|
| Control/Audit API | 暂停、停用、取消、中止、查询和审计导出 | 让用户切换 Agent 模式 |
|
||||||
|
|
||||||
|
## 3. 标识符与幂等键
|
||||||
|
|
||||||
|
| 名称 | 格式/来源 | 唯一性与用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `instanceId` | 固定 `default` | 为未来多实例预留命名空间 |
|
||||||
|
| `repoKey` | `default:<owner>/<repo>` | 仓库锁、工作区和并发分区 |
|
||||||
|
| `resourceKey` | `<repoKey>:<kind>:<number>` | Issue/PR 资源身份 |
|
||||||
|
| `sessionKey` | 同 `resourceKey` | 一个资源一个 Codex App thread |
|
||||||
|
| `deliveryKey` | `default:<X-Gitea-Delivery>` | Webhook 去重;无 header 时使用可靠 payload ID,否则拒绝 |
|
||||||
|
| `taskId` | `gitea-task-` + deliveryKey 的 SHA-256 前缀 | 一次用户评论触发的一次可审计任务;同一 delivery 重放保持不变 |
|
||||||
|
| `threadId` | Codex App 返回值 | 跨任务复用同一资源上下文 |
|
||||||
|
| `turnId` | Codex App 每轮返回值 | 最终回帖确认和恢复边界 |
|
||||||
|
| `commentId` | Gitea 返回值 | 状态/最终/兜底评论幂等引用 |
|
||||||
|
| `leaseId` | UUID + expiry | Scheduler 持有仓库锁和全局槽位的租约 |
|
||||||
|
|
||||||
|
所有唯一键写入必须在同一事务/原子文件替换中完成。重复请求返回原记录,不
|
||||||
|
生成新的 `taskId`、`turnId` 或评论。
|
||||||
|
|
||||||
|
## 4. 持久化数据模型
|
||||||
|
|
||||||
|
字段名是跨模块契约;时间统一 ISO-8601 UTC,所有枚举使用小写 snake_case。
|
||||||
|
|
||||||
|
### 4.1 `webhook_deliveries`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"deliveryKey": "default:8f7…",
|
||||||
|
"deliveryId": "8f7…",
|
||||||
|
"eventType": "issue_comment",
|
||||||
|
"receivedAt": "2026-08-24T08:00:00Z",
|
||||||
|
"signatureValid": true,
|
||||||
|
"normalized": true,
|
||||||
|
"taskId": "task-uuid-or-null",
|
||||||
|
"status": "accepted|ignored|duplicate|rejected",
|
||||||
|
"payloadHash": "sha256:…",
|
||||||
|
"expiresAt": "2026-09-23T08:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`deliveryKey` 唯一;保留期必须覆盖 Gitea 可能的重试窗口。验签失败可只写安全
|
||||||
|
审计,不写入可用于重放的业务 payload。
|
||||||
|
|
||||||
|
### 4.2 `repositories`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"repoKey": "default:acme/widget",
|
||||||
|
"instanceId": "default",
|
||||||
|
"owner": "acme",
|
||||||
|
"name": "widget",
|
||||||
|
"cloneUrl": "https://gitea.example/acme/widget.git",
|
||||||
|
"workspacePath": "/workspace/default/acme/widget",
|
||||||
|
"defaultBranch": "main",
|
||||||
|
"status": "active|disabled|blocked",
|
||||||
|
"registeredAt": "2026-08-24T08:00:00Z",
|
||||||
|
"lastFetchAt": "2026-08-24T08:02:00Z",
|
||||||
|
"version": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注册采用 compare-and-swap:并发首次触发只能产生一条记录。Token 不落在
|
||||||
|
`cloneUrl`;`workspacePath` 必须经过根目录规范化,禁止路径穿越。
|
||||||
|
|
||||||
|
### 4.3 `sessions`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sessionKey": "default:acme/widget:issue:42",
|
||||||
|
"resourceKey": "default:acme/widget:issue:42",
|
||||||
|
"kind": "issue",
|
||||||
|
"number": 42,
|
||||||
|
"threadId": "thread_…",
|
||||||
|
"runtime": {"app": "codexapp", "mode": "yolo"},
|
||||||
|
"mcp": {"server": "gitea", "transport": "stdio", "hostRef": "gitea-main"},
|
||||||
|
"status": "active|waiting_user|closed",
|
||||||
|
"lastTaskId": "task-uuid",
|
||||||
|
"lastTurnId": "turn_…",
|
||||||
|
"createdAt": "2026-08-24T08:03:00Z",
|
||||||
|
"updatedAt": "2026-08-24T08:10:00Z",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`sessionKey` 唯一,`threadId` 创建后不可因普通重试而替换;只有明确恢复失败
|
||||||
|
并经审计后才允许新线程。
|
||||||
|
|
||||||
|
### 4.4 `tasks`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"taskId": "task-uuid",
|
||||||
|
"deliveryKey": "default:8f7…",
|
||||||
|
"repoKey": "default:acme/widget",
|
||||||
|
"resourceKey": "default:acme/widget:issue:42",
|
||||||
|
"sessionKey": "default:acme/widget:issue:42",
|
||||||
|
"commentId": 101,
|
||||||
|
"author": "alice",
|
||||||
|
"instruction": "请修复……",
|
||||||
|
"state": "queued",
|
||||||
|
"attempt": 0,
|
||||||
|
"replyAttempt": 0,
|
||||||
|
"threadId": "thread_…",
|
||||||
|
"turnId": null,
|
||||||
|
"leaseId": null,
|
||||||
|
"nextRetryAt": null,
|
||||||
|
"errorCode": null,
|
||||||
|
"createdAt": "2026-08-24T08:00:01Z",
|
||||||
|
"updatedAt": "2026-08-24T08:00:01Z",
|
||||||
|
"stateVersion": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
任务状态必须与 PRD 第 6 节一致;`stateVersion` 防止并发 worker 的旧写覆盖新写。
|
||||||
|
|
||||||
|
### 4.5 `turns`、`comments`、`audit_events` 与控制面
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"turn": {
|
||||||
|
"turnId": "turn_…",
|
||||||
|
"taskId": "task-uuid",
|
||||||
|
"threadId": "thread_…",
|
||||||
|
"kind": "normal|reply_retry",
|
||||||
|
"status": "started|completed|interrupted|failed|cancelled",
|
||||||
|
"eventSeq": 18,
|
||||||
|
"startedAt": "…",
|
||||||
|
"completedAt": null
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"commentKey": "task-uuid:status:running",
|
||||||
|
"taskId": "task-uuid",
|
||||||
|
"kind": "status|mcp_final|rest_fallback",
|
||||||
|
"resourceKey": "default:acme/widget:issue:42",
|
||||||
|
"turnId": "turn_…",
|
||||||
|
"commentId": 102,
|
||||||
|
"bodyHash": "sha256:…",
|
||||||
|
"status": "pending|sent|confirmed|failed"
|
||||||
|
},
|
||||||
|
"audit_event": {
|
||||||
|
"eventId": "audit-uuid",
|
||||||
|
"taskId": "task-uuid-or-null",
|
||||||
|
"action": "webhook.accept|task.claim|mcp.call|reply.confirm|…",
|
||||||
|
"actor": "system|admin:alice|gitea:user:alice",
|
||||||
|
"fromState": "running",
|
||||||
|
"toState": "verifying_reply",
|
||||||
|
"deliveryId": "8f7…",
|
||||||
|
"turnId": "turn_…",
|
||||||
|
"commentId": 102,
|
||||||
|
"errorCode": null,
|
||||||
|
"timestamp": "…",
|
||||||
|
"metadata": {"attempt": 0}
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"globalPaused": false,
|
||||||
|
"pauseReason": null,
|
||||||
|
"updatedBy": "admin:alice",
|
||||||
|
"updatedAt": "…",
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`commentKey` 是起始/异常结果回执的幂等键;最终回帖以 `taskId+turnId` 为主键,REST
|
||||||
|
兜底不能覆盖已确认的 MCP 评论。
|
||||||
|
|
||||||
|
## 5. 处理时序与事务边界
|
||||||
|
|
||||||
|
### 5.1 Webhook 接收
|
||||||
|
|
||||||
|
1. 读取 raw body 和签名 header;HMAC 失败立即 401。
|
||||||
|
2. 提取 delivery/event header,计算 `payloadHash`;在
|
||||||
|
`webhook_deliveries` 上做唯一插入。
|
||||||
|
3. 已存在 delivery 时返回原处理结果;首次插入后解析事件。
|
||||||
|
4. 过滤非评论、无 mention、Bot 自评论、停用仓库;分别落为
|
||||||
|
`ignored/rejected`,并追加审计。
|
||||||
|
5. 在一个原子操作中登记仓库(若需要)、创建 Session 引用和 Task,状态为
|
||||||
|
`received` → `queued`,写入 delivery 与 task 的关联。
|
||||||
|
6. 立即返回 202(已入队)和 `taskId`;不等待 clone、Codex App 或评论发送。
|
||||||
|
|
||||||
|
### 5.2 调度与工作区
|
||||||
|
|
||||||
|
1. Scheduler 扫描 `queued`、到期 `retry_wait` 和管理员解除的
|
||||||
|
`blocked_workspace`。
|
||||||
|
2. 先检查全局暂停、仓库 active,再以租约方式获取 repo lock。
|
||||||
|
3. CAS 将任务置为 `preparing`;租约含 `leaseId/owner/expiresAt`,心跳续租。
|
||||||
|
4. Workspace Manager 创建目录或执行安全 fetch。失败按错误矩阵决定
|
||||||
|
`blocked_workspace`、`retry_wait` 或 `failed`。
|
||||||
|
5. 任务入队后发送唯一的幂等 `giteabot: 已收到` 起始回执,再将任务置为
|
||||||
|
`running` 并启动/恢复 turn;运行中、等待用户和成功状态不再额外发评论。
|
||||||
|
|
||||||
|
### 5.3 Codex App 与 MCP
|
||||||
|
|
||||||
|
1. 对长驻 app-server 只初始化一次;成功后发送 `initialized` notification。
|
||||||
|
2. best-effort 探测 `experimentalFeature/enablement/set(goals=true)` 和
|
||||||
|
`collaborationMode/list`;探测失败只记日志并保留降级能力。
|
||||||
|
3. `thread/start` 注入工作区 cwd 和线程级 `mcp_servers.gitea`;若使用
|
||||||
|
collaboration mode,`model/reasoning_effort/developer_instructions` 放在
|
||||||
|
`collaborationMode.settings`,顶层不得重复。
|
||||||
|
4. `turn/start` 仅传本轮指令和关联元数据,落库返回的 `turnId`;按事件序号
|
||||||
|
更新 `turns.eventSeq`,重复事件不重复处理。
|
||||||
|
5. turn 完成后根据 stop reason 判断 `waiting_user`、执行失败或进入
|
||||||
|
`verifying_reply`。
|
||||||
|
|
||||||
|
### 5.4 回帖确认与补偿
|
||||||
|
|
||||||
|
1. 仅当 Agent 通过 gitea-mcp 发起最终评论后,才把任务置为
|
||||||
|
`verifying_reply`。
|
||||||
|
2. Reply Verifier 通过 Gitea API/MCP 查询原资源评论,匹配 Bot 作者、
|
||||||
|
`taskId`、`turnId` 和 resourceKey 标记;确认成功则 `succeeded`。
|
||||||
|
3. 只有 Gitea 查询明确返回“评论缺失”时,才将 `replyAttempt += 1` 并最多创建
|
||||||
|
一个 `reply_retry` turn;prompt 明确“只补发上一轮最终文本,不重复修改”。
|
||||||
|
查询异常/状态未知不自动写操作,直接标记 `failed_reply` 并发送状态告警。
|
||||||
|
4. `reply_retry` 仍无法确认时,使用 REST Bot Token 发一条带同样关联标记的兜底评论;
|
||||||
|
成功为 `succeeded_with_rest_fallback`,失败为 `failed_reply`。每一步都写审计。
|
||||||
|
|
||||||
|
### 5.5 重启恢复
|
||||||
|
|
||||||
|
启动恢复扫描按以下顺序执行:
|
||||||
|
|
||||||
|
1. 未过期租约保留 owner;过期租约释放 repo lock 并记录事件。
|
||||||
|
2. `queued` 直接重新入队;`retry_wait` 按 `nextRetryAt` 等待。
|
||||||
|
3. `running/preparing/aborting` 若无活跃 worker,标记
|
||||||
|
`retry_wait(errorCode=interrupted_by_restart)`;超过执行重试上限则 `failed`。
|
||||||
|
4. `waiting_user`、所有终态和已确认评论不自动重跑。
|
||||||
|
5. 恢复过程使用 `stateVersion` CAS,单实例 Scheduler 只能产生一个有效租约。
|
||||||
|
|
||||||
|
### 5.6 规范状态与转移契约
|
||||||
|
|
||||||
|
实现层必须使用以下完整枚举;状态名、终态和转移条件与 PRD 第 6 节保持一致:
|
||||||
|
|
||||||
|
| 当前状态 | 允许的下一状态 | 触发者/守卫 |
|
||||||
|
|---|---|---|
|
||||||
|
| `received` | `queued` / `ignored` / `duplicate` / `rejected` | Webhook Adapter 完成规范化 |
|
||||||
|
| `queued` | `preparing` / `cancelled` | Scheduler 已取得全局槽位和仓库租约;取消请求 |
|
||||||
|
| `preparing` | `running` / `blocked_workspace` / `retry_wait` / `failed` | Workspace/Session Manager 结果 |
|
||||||
|
| `running` | `waiting_user` / `verifying_reply` / `retry_wait` / `aborting` / `failed` | Codex App stop reason、错误或管理员中止 |
|
||||||
|
| `waiting_user` | `queued` / `cancelled` / `failed` | 新评论、取消请求或会话不可恢复 |
|
||||||
|
| `verifying_reply` | `succeeded` / `retry_wait` / `failed_reply` | Reply Verifier 结果;仅允许一次 reply retry |
|
||||||
|
| `retry_wait` | `preparing` / `running` / `verifying_reply` / `failed` | `nextRetryAt` 到期且未超过上限 |
|
||||||
|
| `blocked_workspace` | `queued` / `cancelled` | 管理员确认工作区可用或取消 |
|
||||||
|
| `aborting` | `aborted` / `failed` | Codex App 取消确认或超时 |
|
||||||
|
| `succeeded` / `succeeded_with_rest_fallback` / `failed` / `failed_reply` / `cancelled` / `aborted` / `ignored` / `duplicate` / `rejected` | 无 | 终态,不得创建新 turn |
|
||||||
|
|
||||||
|
每次转移都必须带 `fromState/toState/stateVersion` 和审计事件;未知状态或非法
|
||||||
|
转移拒绝写入并告警。`aborting` 释放资源前不得被误标为 `aborted`,
|
||||||
|
`succeeded_with_rest_fallback` 必须保留原始 MCP 回帖失败原因。
|
||||||
|
|
||||||
|
## 6. 接口契约
|
||||||
|
|
||||||
|
### 6.1 Webhook HTTP
|
||||||
|
|
||||||
|
**Endpoint**:`POST /api/gitea/webhook`(路径可由部署配置映射,但语义固定)
|
||||||
|
|
||||||
|
请求要求:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
X-Gitea-Event: issue_comment
|
||||||
|
X-Gitea-Delivery: 8f7e…
|
||||||
|
X-Gitea-Signature: <HMAC-SHA256(raw_body, webhook_secret)>
|
||||||
|
```
|
||||||
|
|
||||||
|
适配器至少抽取以下规范化结构;原始 payload 只按安全保留策略保存 hash:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"instanceId": "default",
|
||||||
|
"deliveryId": "8f7e…",
|
||||||
|
"eventType": "issue_comment|pull_request_comment",
|
||||||
|
"repo": {"owner": "acme", "name": "widget", "cloneUrl": "…"},
|
||||||
|
"resource": {"kind": "issue|pull_request", "number": 42},
|
||||||
|
"comment": {"id": 101, "body": "@ccweb-bot 请修复…", "createdAt": "…"},
|
||||||
|
"actor": {"login": "alice", "isBot": false},
|
||||||
|
"refs": {"base": "main", "head": "feature/x"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应语义:
|
||||||
|
|
||||||
|
| HTTP | 场景 | 业务效果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 202 | 验签通过且首次入队 | 返回 `{deliveryId, taskId, state:"queued"}` |
|
||||||
|
| 200 | 重复 delivery、合法但忽略的事件 | 返回原 task 或 `{status:"ignored"}` |
|
||||||
|
| 400 | JSON/必需字段无法解析 | 不入队,记录 `invalid_payload` |
|
||||||
|
| 401 | HMAC 缺失或错误 | 不入队,不暴露内部原因 |
|
||||||
|
| 409 | 仓库被管理员停用且策略为拒绝 | 返回 `repository_disabled`,不创建运行任务 |
|
||||||
|
| 500/503 | 持久化不可用 | Gitea 可重试;不得声称已入队 |
|
||||||
|
|
||||||
|
### 6.2 控制与查询 API(逻辑契约)
|
||||||
|
|
||||||
|
实现可映射到现有 cc-web 路由,但字段和幂等语义保持不变:
|
||||||
|
|
||||||
|
| 方法 | 逻辑路径 | 作用 |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/gitea-workflow/overview` | 全局暂停、运行槽位、队列摘要 |
|
||||||
|
| GET | `/api/gitea-workflow/tasks?repoKey=&resourceKey=&state=` | 分页查询任务 |
|
||||||
|
| GET | `/api/gitea-workflow/tasks/:taskId` | 任务、turn、评论和错误详情 |
|
||||||
|
| POST | `/api/gitea-workflow/control/pause` / `resume` | 全局暂停/恢复;body 带 reason |
|
||||||
|
| POST | `/api/gitea-workflow/repos/:repoKey/disable` / `enable` | 仓库停用/启用 |
|
||||||
|
| POST | `/api/gitea-workflow/tasks/:taskId/cancel` | 仅取消 queued/waiting_user |
|
||||||
|
| POST | `/api/gitea-workflow/tasks/:taskId/abort` | 请求中止 running turn |
|
||||||
|
| GET | `/api/gitea-workflow/audit?...` | 按关联 ID/时间范围查询审计 |
|
||||||
|
|
||||||
|
控制请求必须带操作者身份、幂等 `requestId` 和 `reason`;重复 requestId 返回首次
|
||||||
|
结果,不重复发送取消或评论。API 不接收模型、MCP 工具参数或任意 Git 命令。
|
||||||
|
|
||||||
|
### 6.3 Codex App JSON-RPC 形状
|
||||||
|
|
||||||
|
初始化链路:
|
||||||
|
|
||||||
|
```text
|
||||||
|
initialize → initialized(notification)
|
||||||
|
→ experimentalFeature/enablement/set(goals=true) [best effort]
|
||||||
|
→ collaborationMode/list [best effort]
|
||||||
|
→ thread/start
|
||||||
|
```
|
||||||
|
|
||||||
|
线程配置契约(示意):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cwd": "/workspace/default/acme/widget",
|
||||||
|
"collaborationMode": {
|
||||||
|
"settings": {
|
||||||
|
"model": "<固定配置>",
|
||||||
|
"reasoning_effort": "<固定配置>",
|
||||||
|
"developer_instructions": "<工作流系统约束>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mcp_servers": {
|
||||||
|
"gitea": {
|
||||||
|
"command": "gitea-mcp",
|
||||||
|
"args": ["-t", "stdio", "-H", "https://gitea.example"],
|
||||||
|
"env": {"GITEA_ACCESS_TOKEN": "<secret-ref>"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当运行时拒绝 `collaborationMode` 或返回 unknown field 时,记录一次
|
||||||
|
`capability_downgrade`,本轮退回普通 turn;不能因为 goals/list 探测失败而终止
|
||||||
|
app-server。使用 collaboration mode 时,`turn/start` 顶层不得再传 `model` 或
|
||||||
|
`effort`。
|
||||||
|
|
||||||
|
### 6.4 MCP 与 Gitea REST 回帖
|
||||||
|
|
||||||
|
起始/异常回执与最终回帖分离:起始 `已收到` 和必要的异常终态回执由 cc-web
|
||||||
|
通过 Gitea REST 发送,使用 `commentKey=taskId:status:state` 幂等;正常最终答案
|
||||||
|
必须优先由官方 gitea-mcp 发出,REST 只允许作为确认失败后的最终正文兜底。这样
|
||||||
|
既能让用户知道任务已接收,又不会用一串运行状态文本污染 Issue/PR 讨论串。
|
||||||
|
|
||||||
|
架构对官方 gitea-mcp 的具体工具名做一层逻辑适配,避免版本差异泄漏到任务状态:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"operation": "gitea.comment.create",
|
||||||
|
"resource": {"kind": "issue|pull_request", "number": 42},
|
||||||
|
"body": "最终答案\n<!-- ccweb taskId=… turnId=… -->",
|
||||||
|
"idempotencyKey": "task-uuid:turn_…"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
适配器必须将工具返回映射为 `{accepted, commentId, errorCode}`,并再次查询确认。
|
||||||
|
REST 兜底使用 Bot Token 调用 Gitea 的对应 Issue/PR 评论接口,body 必须复用
|
||||||
|
`taskId/turnId` 标记;若 REST 返回 2xx 但查询不到,状态仍为 `failed_reply`,不能
|
||||||
|
虚报成功。
|
||||||
|
|
||||||
|
## 7. 并发、租约与一致性
|
||||||
|
|
||||||
|
- 调度只依赖 `repoKey` 级 lease;没有全局 semaphore 或全局并发槽位。
|
||||||
|
- 仓库 lease 的键为 `repoKey`,TTL 必须短于 worker 心跳间隔的可容忍失联窗口;
|
||||||
|
释放必须校验 `leaseId`,防止旧 worker 解锁新 worker 的租约。
|
||||||
|
- 领取任务采用 `stateVersion` CAS:`queued → preparing` 成功者才可继续。
|
||||||
|
- 任务状态、评论幂等记录和审计事件先写入,再执行外部调用;外部调用结果以
|
||||||
|
append-only 事件补写,重试读取最后已知结果。
|
||||||
|
- 进程内锁只作性能优化,正确性依赖持久 lease;单实例 MVP 仍按可恢复模型设计,
|
||||||
|
以便未来横向扩展。
|
||||||
|
|
||||||
|
## 8. 失败场景矩阵
|
||||||
|
|
||||||
|
| 场景 | 检测 | 状态/动作 | 是否重试 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| HMAC 缺失/错误 | 常量时间校验失败 | 401、`rejected` 审计 | 否 |
|
||||||
|
| delivery 重复 | 唯一键冲突 | 返回原结果、`duplicate` | 否 |
|
||||||
|
| payload 不完整 | schema 校验失败 | 400、`invalid_payload` | 否 |
|
||||||
|
| 仓库停用 | `repositories.status=disabled` | 拒绝新任务 | 否 |
|
||||||
|
| clone/fetch 鉴权失败 | Git exit code/HTTP 401 | `retry_wait`,超过上限 `failed` | 最多 1 次 |
|
||||||
|
| 工作区脏或冲突 | git status/lock 检查 | `blocked_workspace`,不 reset | 管理员解除后 1 次 |
|
||||||
|
| 全局暂停 | control CAS | 保持 `queued`,不领取 | 恢复后 |
|
||||||
|
| 租约过期 | TTL/心跳超时 | 释放资源并恢复扫描 | 最多 1 次 |
|
||||||
|
| Codex App 不可达 | JSON-RPC timeout | `retry_wait` | 最多 1 次 |
|
||||||
|
| gitea-mcp 命令缺失 | 启动前可执行文件检查失败 | `failed` + 一条 `giteabot:` 失败结果回执 | 不重试,修复命令后重新触发 |
|
||||||
|
| MCP 进程启动失败 | stdio 子进程启动即退出 | `failed` + 一条 `giteabot:` 失败结果回执 | 不重试,修复命令后重新触发 |
|
||||||
|
| MCP initialize 握手失败/超时 | stdio JSON-RPC `initialize` 无成功响应 | `failed` + 一条 `giteabot:` 失败结果回执 | 不重试,修复命令后重新触发 |
|
||||||
|
| turn 被重启中断 | 无活跃 worker | `retry_wait(interrupted_by_restart)` | 最多 1 次 |
|
||||||
|
| 用户补充问题 | stop reason/waiting 信号 | `waiting_user`,保留 thread | 新评论触发 |
|
||||||
|
| 管理员中止超时 | cancel 无确认 | `failed` 或 `aborted`,强制释放 lease 并审计 | 否 |
|
||||||
|
| MCP 最终回帖缺失 | 查询无匹配评论 | 一次 `reply_retry` turn | 仅 1 次 |
|
||||||
|
| REST 兜底失败 | 非 2xx/查询不到 | `failed_reply` + 告警 | 不自动重试 |
|
||||||
|
| Bot 自评论回调 | actor/login 匹配 Bot | `ignored` | 否 |
|
||||||
|
|
||||||
|
错误码建议使用稳定小写值(如 `invalid_signature`、`duplicate_delivery`、
|
||||||
|
`workspace_dirty`、`app_server_timeout`、`mcp_reply_missing`),展示文本可本地化。
|
||||||
|
|
||||||
|
## 9. 安全与审计设计
|
||||||
|
|
||||||
|
### 9.1 信任边界
|
||||||
|
|
||||||
|
Webhook Secret 只在入口使用;Bot Token 只在 Gitea MCP/REST 出站边界使用;
|
||||||
|
Codex App 收到的评论、Issue 描述、代码和 PR 内容全部视为不可信数据。系统提示
|
||||||
|
明确禁止将这些内容解释为更改运行时权限、读取 Secret 或关闭审计的指令。
|
||||||
|
|
||||||
|
### 9.2 权限与凭据
|
||||||
|
|
||||||
|
- 官方 gitea-mcp 按已确认需求开放写权限,但通过固定实例 host、专用 Bot Token、
|
||||||
|
运维暂停/中止和完整审计进行约束。
|
||||||
|
- 日志仅记录 `hostRef/tokenRef`,不记录 Authorization、完整命令环境或原始 Secret。
|
||||||
|
- Git clone/fetch 的凭据使用短生命周期注入,完成后清理;remote URL 不含 Token。
|
||||||
|
|
||||||
|
### 9.3 审计完整性
|
||||||
|
|
||||||
|
每个外部副作用前写入 intent 事件,完成后写入 result 事件;事件不可更新,只能
|
||||||
|
追加补偿或人工操作事件。审计关联链必须可由
|
||||||
|
`deliveryKey → taskId → sessionKey/threadId → turnId → commentId` 重建。
|
||||||
|
|
||||||
|
## 10. 验收与实现交接
|
||||||
|
|
||||||
|
实现方需按 [PRD.md](./PRD.md) 的 AC-01~AC-13 编写协议 mock、状态迁移、重启、
|
||||||
|
并发、回帖确认和安全测试。任何无法满足的契约必须在对应审计/错误码中显式降级,
|
||||||
|
不得静默跳过。
|
||||||
|
|
||||||
|
实现已按本文边界落地;后续修改不得绕过验签、状态机、幂等键和回帖核验,也不得
|
||||||
|
新增未审计的外部写操作。
|
||||||
84
docs/gitea-workflow/CODEX-INTEGRATION.md
Normal file
84
docs/gitea-workflow/CODEX-INTEGRATION.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Codex App / Gitea Workflow 挂接说明
|
||||||
|
|
||||||
|
本文件对应 `lib/gitea-workflow-codex.js` 的独立协议适配器,以及 `server.js`
|
||||||
|
中保持薄层的三个挂接点。
|
||||||
|
|
||||||
|
## 线程级 gitea-mcp
|
||||||
|
|
||||||
|
Webhook runner 为每个任务创建工作区和 token 后,将如下对象随本轮消息传入:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
giteaWorkflow: {
|
||||||
|
taskId: task.taskId,
|
||||||
|
kind: 'normal',
|
||||||
|
mcp: { host: 'https://gitea.example', accessToken: token },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`codexAppThreadConfig()` 会将其合并为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcp_servers.gitea": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "gitea-mcp",
|
||||||
|
"args": ["-t", "stdio", "-H", "https://gitea.example"],
|
||||||
|
"env": {
|
||||||
|
"GITEA_HOST": "https://gitea.example",
|
||||||
|
"GITEA_ACCESS_TOKEN": "<线程级凭据>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
token 不写入 app-server 进程环境,也不通过 dynamicTools 注入。使用
|
||||||
|
`collaborationMode` 时,模型与推理参数仍只放在 `settings`。
|
||||||
|
|
||||||
|
`gitea-mcp` 二进制随 cc-web 交付,源码部署默认查找
|
||||||
|
`<cc-web>/bin/gitea-mcp`,single-exe 部署默认查找
|
||||||
|
`<发布目录>/bin/gitea-mcp`。只有需要替换官方版本时才配置
|
||||||
|
`CC_WEB_GITEA_MCP_COMMAND` 指向本机绝对路径;不要求每个服务各自安装一份。
|
||||||
|
命令不可执行时,任务在启动 Codex App 前以 `gitea_mcp_not_found` 快速失败,不会
|
||||||
|
让对话长期停留在 `running`。
|
||||||
|
|
||||||
|
在启动 Codex App 前,cc-web 还会用同一份线程级 command/args/env 做一次最小
|
||||||
|
stdio `initialize` 预检。预检只验证进程能启动并返回 JSON-RPC initialize 响应,
|
||||||
|
不调用 Gitea 工具;进程启动失败、返回 MCP error 或在
|
||||||
|
`CC_WEB_GITEA_MCP_STARTUP_TIMEOUT_MS`(默认 8 秒)内无响应时,分别记录
|
||||||
|
`gitea_mcp_start_failed`、`gitea_mcp_handshake_failed` 或
|
||||||
|
`gitea_mcp_handshake_timeout`,任务进入 `failed`,并由统一队列监听发送一条
|
||||||
|
`giteabot: 任务失败:...` 回执。cc-web 不会自动下载或安装 gitea-mcp。
|
||||||
|
|
||||||
|
## turn 生命周期
|
||||||
|
|
||||||
|
- `startCodexAppTurn()` 在 `turn/start` 返回后调用适配器的
|
||||||
|
`handleTurnStarted({ taskId, threadId, turnId, kind })`。
|
||||||
|
- `handleCodexAppTurnComplete()` 保留原有消息落盘和生命周期广播,并让既有
|
||||||
|
Gitea waiter 调用 `settleGiteaWorkflowTurn()` 做回执查询、一次隐藏补触发和
|
||||||
|
REST 兜底。
|
||||||
|
- 运行时通知可用 `extractTurnId()` / `extractThreadId()` 兼容
|
||||||
|
`params.turnId`、`params.turn.id`、`params.item.turnId` 等形状。
|
||||||
|
|
||||||
|
## 回执与 waiting_user
|
||||||
|
|
||||||
|
`buildWorkflowMarker()` 生成版本化 HTML 注释:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ccweb-gitea v="1" taskId="..." turnId="..." resourceKey="..." kind="final" -->
|
||||||
|
```
|
||||||
|
|
||||||
|
只有同时匹配资源、taskId、turnId(含补发轮次允许的历史标识集合)和 Bot 身份的评论才算
|
||||||
|
`confirmed`。查询异常为 `unknown`,不得据此再次执行修改、补发或 REST 覆盖;只有确认
|
||||||
|
评论缺失才允许同一 thread 发起一次 `reply_retry`,其提示词只允许补发上一轮最终文本,
|
||||||
|
仍未确认才进入 REST 兜底。
|
||||||
|
|
||||||
|
Agent 需要澄清或报告阻塞时使用同一标识结构但将 `kind` 设为 `waiting_user`。cc-web
|
||||||
|
确认该标识后直接把任务置为 `waiting_user`,不触发回帖补发;用户再次 `@ccweb-bot`
|
||||||
|
时创建新 turn,并复用原 Issue/PR 的 session/thread。
|
||||||
|
|
||||||
|
`queueWaitingUserComment()` 为用户的后续评论创建新 task/turn,但复用父任务的
|
||||||
|
`sessionKey` 与 `threadId`;Bot 自评论按稳定 user id 优先、login 兜底过滤。
|
||||||
|
带有完整 `ccweb-gitea` 隐藏标记的评论即使作者身份字段缺失,也按自身回执忽略,
|
||||||
|
避免回执 Webhook 形成循环。
|
||||||
147
docs/gitea-workflow/DEPLOYMENT.md
Normal file
147
docs/gitea-workflow/DEPLOYMENT.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Gitea Webhook Agent 部署与运维设计
|
||||||
|
|
||||||
|
> 本文是 [PRD.md](./PRD.md) 与 [ARCHITECTURE.md](./ARCHITECTURE.md) 的运行手册。
|
||||||
|
> 它定义部署、凭据、恢复和故障处置边界;Gitea 仍是唯一主交互入口,cc-web 页面
|
||||||
|
> 只承担配置、镜像和运维控制。
|
||||||
|
|
||||||
|
## 1. 运行基线
|
||||||
|
|
||||||
|
MVP 采用单 Gitea 实例(`instanceId=default`)和专用 `ccweb-bot`。同仓库永远
|
||||||
|
串行,跨仓库允许并行,不设置全局并发上限。生产环境至少需要:
|
||||||
|
|
||||||
|
| 项目 | 基线 |
|
||||||
|
|---|---|
|
||||||
|
| Node/Bun | 使用项目锁定的运行时和已验收的单文件发布包;不要混用系统 Node |
|
||||||
|
| 进程 | 一个 cc-web worker;外部 Codex App Server 按会话复用 |
|
||||||
|
| 持久目录 | `WORKFLOW_DATA_ROOT`(任务/审计/租约)与 `WORKSPACE_ROOT`(Git 工作区)分离 |
|
||||||
|
| 网络 | Gitea Webhook 入站、Gitea REST/MCP 出站、Codex App Server 本地 stdio/JSON-RPC |
|
||||||
|
| 时间 | 主机启用 NTP;持久化时间全部使用 UTC ISO-8601 |
|
||||||
|
| 访问 | 反向代理终止 TLS;Webhook 路径只允许 POST 和受限请求体大小 |
|
||||||
|
|
||||||
|
建议为 worker、工作区和日志使用独立系统用户。`WORKFLOW_DATA_ROOT` 仅 worker
|
||||||
|
可读写,`WORKSPACE_ROOT` 不允许 Web 静态服务暴露,备份目录使用不同权限主体。
|
||||||
|
|
||||||
|
## 2. 配置与凭据
|
||||||
|
|
||||||
|
配置按“非敏感文件 → Secret 管理 → 线程级注入”分层。示例(值均为占位符):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
CC_WEB_GITEA_HOST=https://gitea.example
|
||||||
|
CC_WEB_GITEA_INSTANCE_ID=default
|
||||||
|
CC_WEB_GITEA_BOT_LOGIN=ccweb-bot
|
||||||
|
GITEA_WEBHOOK_SECRET_FILE=/etc/ccweb/secrets/gitea-webhook
|
||||||
|
GITEA_BOT_TOKEN_FILE=/etc/ccweb/secrets/gitea-bot-token
|
||||||
|
CC_WEB_GITEA_WORKFLOW_STATE=/var/lib/ccweb/gitea-workflow/state.json
|
||||||
|
CC_WEB_GITEA_WORKSPACE_ROOT=/var/lib/ccweb/workspaces
|
||||||
|
```
|
||||||
|
|
||||||
|
cc-web 管理页对应接口为:
|
||||||
|
|
||||||
|
- `GET /api/gitea-workflow/config`:返回地址、Bot 用户名、工作区和凭据是否已配置;不会返回 Secret/Token。
|
||||||
|
- `PUT /api/gitea-workflow/config`:保存上述配置,敏感字段写入受保护的运行配置;保存后立即更新当前进程的 Webhook、Git 和 gitea-mcp 线程配置,不要求重启 cc-web。
|
||||||
|
- `GET /api/gitea-workflow/overview`、`/tasks`、`/audit`:状态镜像。
|
||||||
|
- `POST /api/gitea-workflow/control/pause|resume`、仓库 `enable|disable`、任务 `cancel|abort`:需要 cc-web 登录 Bearer Token;服务端自动记录操作来源和请求幂等 ID,页面不要求用户填写审计字段。
|
||||||
|
|
||||||
|
- 内部部署可以不配置 Webhook Secret;配置后入口启用 HMAC-SHA256 验签。Bot Token
|
||||||
|
只在 gitea-mcp/REST 出站边界使用,不能把 Bot Token 当作 Webhook Secret。
|
||||||
|
- Secret 文件权限为 `0600`、属主为 worker;日志、审计、评论正文和 Git remote
|
||||||
|
URL 禁止写入 Secret。审计只记录 `secretRef/tokenRef` 和不可逆指纹。
|
||||||
|
- `thread/start.config.mcp_servers.gitea.env` 使用线程级 secret 引用或运行时
|
||||||
|
注入值;不能把某个来源会话 ID、Bot Token 或 Gitea Secret 放在长驻
|
||||||
|
app-server 的进程级全局环境中。
|
||||||
|
- `gitea-mcp` 随 cc-web 源码/发布目录放在 `bin/gitea-mcp`,不需要为每个服务单独
|
||||||
|
安装。只有替换版本时才使用 `CC_WEB_GITEA_MCP_COMMAND` 指向本机绝对路径;可选的
|
||||||
|
`CC_WEB_GITEA_MCP_STARTUP_TIMEOUT_MS` 控制 initialize 预检超时(默认 8000ms,
|
||||||
|
上限 30000ms)。cc-web 发布流程不从网络自动下载或安装外部二进制。
|
||||||
|
- 轮换 Secret 时先配置新值并验证签名,再撤销旧值;未配置 Secret 的内部入口应
|
||||||
|
由反向代理/网络白名单限制来源。Bot Token 更新后立即作用于后续 MCP 线程。
|
||||||
|
|
||||||
|
## 3. Webhook 与反向代理
|
||||||
|
|
||||||
|
Gitea 只向反向代理暴露的 HTTPS 路径发送 Webhook。代理应:
|
||||||
|
|
||||||
|
1. 严格保留原始请求 body 和 `X-Gitea-Signature`、`X-Gitea-Delivery`、
|
||||||
|
`X-Gitea-Event` 头,不做 JSON 重排后再交给验签层。
|
||||||
|
2. 限制方法为 POST、请求体大小(建议 1 MiB)和连接超时;超限返回 413。
|
||||||
|
3. 只将来自 Gitea 网段的请求转发到 worker;不要在代理层伪造签名或 delivery。
|
||||||
|
4. 配置了 Secret 时,worker 验签失败返回 401;解析失败返回 400,首次入队返回
|
||||||
|
202,重复 delivery/忽略事件返回 200。未配置 Secret 的内部入口仍需由代理
|
||||||
|
限制来源;持久化不可用时返回 503。
|
||||||
|
5. 代理访问日志脱敏 `Authorization`、签名值和正文;保留 request ID 以便与
|
||||||
|
`deliveryKey` 关联。
|
||||||
|
|
||||||
|
验签必须针对 raw body 计算 HMAC-SHA256,并使用常量时间比较。解析后的 mention
|
||||||
|
和仓库字段不能替代 raw body 验签;缺少 delivery header 时只能使用可靠 payload
|
||||||
|
ID,否则拒绝入队。
|
||||||
|
|
||||||
|
## 4. 启动、健康检查与发布
|
||||||
|
|
||||||
|
启动顺序固定为:
|
||||||
|
|
||||||
|
1. 检查配置文件、Secret 文件权限、数据目录可写性和工作区根目录是否在允许
|
||||||
|
根下;禁止自动 `reset --hard` 或 `clean`。
|
||||||
|
2. 打开持久 Store,执行租约过期扫描和恢复扫描;`queued` 重新入队,
|
||||||
|
`running/preparing/aborting` 按架构规则转为带 `interrupted_by_restart` 的
|
||||||
|
`retry_wait`,终态不重跑。
|
||||||
|
3. 启动 Scheduler 和受控的 Codex App Server;完成 `initialize` 后发送
|
||||||
|
`initialized`,再 best-effort 探测 goals 与 collaboration mode。
|
||||||
|
4. 仅在健康检查通过后把 Webhook 代理切入当前 worker。
|
||||||
|
|
||||||
|
健康检查至少包含:Store 读写探针、租约扫描完成、Gitea `/api/v1/version` 或
|
||||||
|
等价只读探针、Codex App Server initialize smoke,以及线程级 gitea-mcp
|
||||||
|
stdio `initialize` 预检。健康检查不得创建评论、修改仓库或打印凭据。
|
||||||
|
|
||||||
|
发布采用“先旁路验证、再切流”:在 staging 用回归 fixtures 和真实 Gitea 测试
|
||||||
|
仓库验证 HMAC、入队、MCP 配置与回帖确认;生产切换前确认旧 worker 已停止领取
|
||||||
|
新任务且租约已释放。回滚只切换到上一份已验收发布包和数据快照,不删除当前
|
||||||
|
工作区或审计。
|
||||||
|
|
||||||
|
## 5. 日常运维控制
|
||||||
|
|
||||||
|
| 操作 | 影响 | 约束 |
|
||||||
|
|---|---|---|
|
||||||
|
| 全局 pause | 停止领取新任务,已运行任务继续 | 页面按钮直接执行,服务端记录操作 |
|
||||||
|
| resume | 恢复调度 | 先检查工作区和 Gitea 连通性 |
|
||||||
|
| repo disable | 拒绝该仓库新触发 | 不强杀运行中任务,保留历史 |
|
||||||
|
| cancel queued | 取消未启动任务 | 幂等;不得取消已领取任务 |
|
||||||
|
| abort running | 向 Codex App 请求取消 | 等待确认或超时后释放租约并审计 |
|
||||||
|
| replay delivery | 仅用于核对/恢复 | 先查 `deliveryKey`,禁止绕过去重直接执行 |
|
||||||
|
|
||||||
|
所有控制操作必须通过受保护的运维 API;服务端生成操作记录和幂等 ID。API 不接受
|
||||||
|
任意 Git 命令、模型切换或 MCP 工具参数。
|
||||||
|
|
||||||
|
## 6. 监控与告警
|
||||||
|
|
||||||
|
结构化日志只输出关联 ID:`deliveryKey、taskId、repoKey、sessionKey、threadId、
|
||||||
|
turnId、leaseId、errorCode`。建议监控:Webhook 401/400/503 比例、队列年龄、锁等待、
|
||||||
|
运行任务数量、`retry_wait` 数量、`blocked_workspace` 数量、MCP 握手失败、回帖确认
|
||||||
|
缺失和 REST 兜底次数。
|
||||||
|
|
||||||
|
以下情况应告警并暂停自动扩容:连续 HMAC 失败、同一 delivery payload hash
|
||||||
|
变化、租约频繁过期、回帖兜底连续失败、工作区出现未知 owner 或路径越界、
|
||||||
|
Bot Token/Secret 可能泄露。告警正文不得包含 Secret、完整评论正文或源码。
|
||||||
|
|
||||||
|
## 7. 备份、恢复与数据保留
|
||||||
|
|
||||||
|
- 备份 Store 的任务、会话、turn、评论索引、控制面和 append-only 审计;
|
||||||
|
`webhook_deliveries` 至少保留覆盖 Gitea 重试窗口的记录。
|
||||||
|
- 工作区是可重建缓存,备份前先记录 `repoKey、HEAD、dirty 状态`;不要把
|
||||||
|
未提交用户修改静默覆盖或当成可恢复快照。
|
||||||
|
- 恢复顺序:停止领取 → 恢复 Store → 校验版本/唯一键 → 执行租约恢复扫描 →
|
||||||
|
只读检查工作区 → 启动 Scheduler → 逐步 resume。恢复后优先核对
|
||||||
|
`deliveryKey → taskId → turnId → commentId` 链路,避免重复回帖。
|
||||||
|
- 清理策略必须由管理员显式启用并保留审计;不得因磁盘告警直接删除运行中
|
||||||
|
任务、未确认评论或工作区。
|
||||||
|
|
||||||
|
## 8. 故障处置速查
|
||||||
|
|
||||||
|
| 症状 | 先查 | 安全动作 |
|
||||||
|
|---|---|---|
|
||||||
|
| Webhook 全部 401 | Secret 引用、raw body、代理头 | 暂停切流,验证单个 fixture,不打印签名 |
|
||||||
|
| 任务堆积 | pause、锁、并发槽位、租约 | 不手工改状态;修复后让恢复扫描接管 |
|
||||||
|
| 工作区 blocked | `git status`、owner、锁文件 | 保留现场,管理员审查后再解除;禁止 reset/clean |
|
||||||
|
| App/MCP 不可达 | initialize、stdio stderr、hostRef、`gitea-mcp` 可执行文件 | 命令缺失、进程启动失败、握手失败或超时均立即进入 `failed` 并发送一条 `giteabot:` 失败回执;修复后重新触发 |
|
||||||
|
| MCP 成功但无评论 | resource/task/turn 标记、Gitea 查询 | 只允许一次 reply_retry,随后 REST 兜底 |
|
||||||
|
| 重启后重复执行 | stateVersion、leaseId、终态记录 | 先 pause,修复 Store/租约,再恢复;禁止批量重放 |
|
||||||
|
|
||||||
|
每次事故结束后导出关联审计、错误码、重试次数和最终评论 ID,形成可复盘记录。
|
||||||
255
docs/gitea-workflow/PRD.md
Normal file
255
docs/gitea-workflow/PRD.md
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
# Gitea Webhook 驱动 ccweb 工作流 PRD
|
||||||
|
|
||||||
|
> 文档状态:已确认需求基线(MVP)
|
||||||
|
> 适用范围:单个 Gitea 实例、一个全局 `ccweb-bot` 账号
|
||||||
|
> 相关设计:[ARCHITECTURE.md](./ARCHITECTURE.md)
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
本功能把 Gitea Issue/PR 的普通评论作为 ccweb 的任务入口。用户在评论中
|
||||||
|
提及 `@ccweb-bot` 后,cc-web 验证 Webhook、准备持久工作区、复用该
|
||||||
|
Issue/PR 的独立 Codex App 会话,并通过官方 `gitea-mcp` 完成代码研究、修改
|
||||||
|
和回帖。Gitea 是唯一主交互入口;cc-web 页面只提供镜像、运维和审计能力。
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
1. 让用户无需离开 Gitea 即可驱动一次或连续多轮工程任务。
|
||||||
|
2. 保证同一仓库的工作区和任务严格串行,不同仓库可并行;MVP 不设置额外的
|
||||||
|
全局并发上限。
|
||||||
|
3. 在进程重启、网络抖动、MCP 回帖失败时,任务状态、会话和回执仍可恢复、
|
||||||
|
重试且不重复执行或重复回帖。
|
||||||
|
4. 对 Webhook、Bot Token、工作区写入和每次工具调用提供可追溯审计,并允许运
|
||||||
|
维人员暂停、取消排队和中止运行中的 turn。
|
||||||
|
|
||||||
|
## 2. 已确认决策
|
||||||
|
|
||||||
|
| 主题 | 决策 |
|
||||||
|
|---|---|
|
||||||
|
| Gitea 实例 | MVP 只支持一个配置的 Gitea 实例,`instanceId=default` |
|
||||||
|
| Bot | 一个全局 `ccweb-bot`,用于触发识别、起始回执和 REST 兜底 |
|
||||||
|
| 触发 | Issue 或 PR 的普通评论正文包含 `@ccweb-bot`;评论作者不能是 Bot 自身 |
|
||||||
|
| 自动接入 | 合法触发首次发现未登记仓库时自动登记并 clone;管理员可停用 |
|
||||||
|
| 工作区 | 可配置工作区根目录下按仓库持久 checkout,后续使用 fetch 更新 |
|
||||||
|
| 并发 | 同仓库串行;跨仓库并行;MVP 不设置全局运行上限 |
|
||||||
|
| Agent | 固定 Codex App(`codexapp`)+ `yolo`,不在 Gitea 交互中暴露模式切换 |
|
||||||
|
| MCP | 官方 `gitea-mcp`,stdio 传输;线程级注入 Gitea host/token |
|
||||||
|
| 会话 | 每个 Issue/PR 一个独立持久会话;后续同资源评论进入同一会话 |
|
||||||
|
| 交互 | Gitea 为主;cc-web 页面只展示状态、队列和审计,不替代 Gitea 对话 |
|
||||||
|
| 回复 | 先发一条“已收到”回执;正常最终答案由 gitea-mcp 发回原 Issue/PR;缺失时一次补触发,最后 REST 兜底 |
|
||||||
|
| 安全 | Webhook HMAC、delivery 去重、Bot 自评论过滤、Token/Secret 分离、审计 |
|
||||||
|
| 恢复 | `queued` 重启后继续;`waiting_user` 保留;`running` 视为中断并最多重试一次 |
|
||||||
|
| 运维 | 全局暂停、仓库停用、取消排队、中止 turn、查看审计 |
|
||||||
|
|
||||||
|
## 3. 范围
|
||||||
|
|
||||||
|
### 3.1 In Scope
|
||||||
|
|
||||||
|
- Gitea Webhook 接收、原始请求 HMAC 校验和 delivery 幂等。
|
||||||
|
- Issue/PR 普通评论事件的规范化、提及解析和 Bot 自评论抑制。
|
||||||
|
- 未登记仓库自动接入、持久 clone/fetch、脏工作区保护。
|
||||||
|
- 任务队列、同仓库锁、跨仓库并行。
|
||||||
|
- Issue/PR 独立会话与 Codex App turn 生命周期管理。
|
||||||
|
- 线程级官方 `gitea-mcp` stdio 配置和 Gitea 主交互。
|
||||||
|
- 起始“已收到”回执、MCP 最终回帖确认、一次补触发和 REST 兜底;不发送运行中、等待用户或成功状态评论。
|
||||||
|
- 暂停、停用、取消排队、中止、重启恢复、审计和可观测字段。
|
||||||
|
- 可供前端/运维使用的只读状态和控制接口契约(实现不在本 PRD 变更范围)。
|
||||||
|
|
||||||
|
### 3.2 Out of Scope
|
||||||
|
|
||||||
|
- 多 Gitea 实例、跨 Forge 统一协议和 GitHub/GitLab 适配。
|
||||||
|
- 非评论触发(Push、定时任务、Issue 创建、Actions 等)。
|
||||||
|
- 让普通用户在 cc-web 中切换模型、推理强度或 yolo/plan 模式。
|
||||||
|
- 自定义 dynamicTools 代替 MCP;手动填写 MCP 工具参数的 UI。
|
||||||
|
- 自动清理用户未提交修改、`reset --hard`、强制覆盖其他会话的工作区。
|
||||||
|
- 细粒度按用户/路径的写权限策略(Bot 全写权限是已确认前提);只记录风险并提供暂停/审计。
|
||||||
|
|
||||||
|
## 4. 角色与核心场景
|
||||||
|
|
||||||
|
| 角色 | 能力 |
|
||||||
|
|---|---|
|
||||||
|
| Gitea 用户 | 在 Issue/PR 评论 `@ccweb-bot`,查看状态、追问、确认结果 |
|
||||||
|
| `ccweb-bot` | 发送起始回执、作为 MCP/REST 身份回帖;Bot 自己的评论不会再次触发 |
|
||||||
|
| 运维管理员 | 全局暂停/恢复、仓库停用/启用、取消排队、中止 turn、查审计 |
|
||||||
|
| 系统 | 验签、去重、排队、clone/fetch、启动会话、确认回帖、重试和恢复 |
|
||||||
|
|
||||||
|
典型链路:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gitea 普通评论 @ccweb-bot
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
验签 → delivery 去重 → 解析 Issue/PR → 自动接入仓库
|
||||||
|
│ │
|
||||||
|
└────────────── 持久化任务并入队 ◄──┘
|
||||||
|
│
|
||||||
|
同仓库锁串行,跨仓并行 ────┘
|
||||||
|
▼
|
||||||
|
Codex App/yolo + gitea-mcp(stdio)
|
||||||
|
│
|
||||||
|
已收到回执 → MCP 最终回帖 → turnId/评论确认
|
||||||
|
│
|
||||||
|
补触发一次 → REST 兜底(必要时)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 功能需求
|
||||||
|
|
||||||
|
### FR-01 触发与事件规范化
|
||||||
|
|
||||||
|
- 接受 Gitea 配置的 Issue/PR 评论 Webhook;事件适配器将不同版本的事件名称
|
||||||
|
归一为 `issue_comment` 或 `pull_request_comment`。
|
||||||
|
- 仅当正文包含独立 mention `@ccweb-bot` 时触发,mention 后的文本作为任务
|
||||||
|
指令;没有有效指令时仍创建可追踪任务并要求用户补充。
|
||||||
|
- 记录原始 `deliveryId`、事件类型、仓库、资源类型/编号、评论 ID、作者和
|
||||||
|
接收时间。无法解析的事件进入 `ignored`,不启动 Agent。
|
||||||
|
- Bot 自己发出的状态/最终/兜底评论必须被识别并忽略,防止自触发环路。
|
||||||
|
|
||||||
|
### FR-02 Webhook 安全与幂等
|
||||||
|
|
||||||
|
- 在读取或持久化业务字段前,使用原始 body 和配置 Secret 计算 HMAC-SHA256,
|
||||||
|
采用常量时间比较;验签失败返回 401,不入队。
|
||||||
|
- `deliveryKey = instanceId + ":" + deliveryId` 唯一;已处理或处理中重复
|
||||||
|
delivery 返回 200(或约定的幂等响应)但不创建新任务。
|
||||||
|
- Secret、Bot Token、MCP 启动参数不得写入评论、普通日志或审计明文;审计只
|
||||||
|
记录凭据引用/哈希指纹。
|
||||||
|
|
||||||
|
### FR-03 仓库自动接入与工作区
|
||||||
|
|
||||||
|
- 通过合法 Webhook 首次发现仓库时原子创建 `RepositoryRecord`;未登记不因
|
||||||
|
任务排队失败而丢失事件。
|
||||||
|
- 按 `workspaceRoot/<instanceId>/<owner>/<repo>` 建立持久目录。首次使用
|
||||||
|
`clone`,后续任务在仓库锁内 `fetch`;凭据通过临时环境或 credential helper
|
||||||
|
注入,不把 Token 写入 remote URL。
|
||||||
|
- 检测到未提交修改、冲突或目录被外部占用时,不执行 reset/clean/覆盖;任务
|
||||||
|
进入 `blocked_workspace`,写入审计并等待管理员处理。
|
||||||
|
- PR 任务必须记录 base/head ref 和源仓库信息;Issue 任务使用仓库默认分支,
|
||||||
|
除非指令明确指定已允许的分支。
|
||||||
|
|
||||||
|
### FR-04 队列、锁与并发
|
||||||
|
|
||||||
|
- 调度器维护持久任务队列;同 `repoKey` 同时最多一个 `preparing/running`
|
||||||
|
任务,跨仓库可并行。
|
||||||
|
- 不设置额外全局运行信号量;`queued` 任务只受全局暂停和各自仓库锁影响,暂停时不再领取新任务。
|
||||||
|
- 队列顺序默认 FIFO;同一资源的新评论可合并为下一轮输入,但不得跳过已持久
|
||||||
|
化的任务或破坏评论顺序。
|
||||||
|
- 取消排队只允许作用于尚未启动的任务;中止操作必须向 Codex App 发送取消,
|
||||||
|
并等待 `aborted` 或超时后释放锁。
|
||||||
|
|
||||||
|
### FR-05 会话与 Agent 运行时
|
||||||
|
|
||||||
|
- `sessionKey = instanceId:owner/repo:kind:number`,其中 `kind` 为 `issue` 或
|
||||||
|
`pull_request`;同一资源永远复用同一个 `threadId`。
|
||||||
|
- 每个新任务创建一个 `turnId`;持久化 `threadId/turnId`、启动时间、最后事件
|
||||||
|
序号和重试次数,支持断线后恢复监听。
|
||||||
|
- 启动参数固定为 `codexapp` 与 `yolo`。`model`、`reasoning_effort`、
|
||||||
|
`developer_instructions` 放入 `collaborationMode.settings`(若运行时启用
|
||||||
|
collaborationMode),不得在顶层重复传递。
|
||||||
|
- `thread/start.config.mcp_servers.gitea` 线程级注入官方 gitea-mcp stdio;每
|
||||||
|
个线程使用对应实例的 host/token,不使用进程全局来源会话变量替代。
|
||||||
|
- Prompt 必须包含仓库/资源标识、评论上下文、工作区路径、预期回帖格式和安全
|
||||||
|
边界;评论正文视为不可信输入,不得覆盖系统约束。
|
||||||
|
|
||||||
|
### FR-06 Gitea 交互与回帖确认
|
||||||
|
|
||||||
|
- 合法任务只发送一条起始 `giteabot: 已收到` 回执;运行中、等待用户和成功状态
|
||||||
|
不再额外发送评论,避免污染 Issue/PR 讨论串。回执仍带不可见关联标记
|
||||||
|
(`taskId`、`state`、可选 `turnId`)用于去重和审计。
|
||||||
|
- 正常最终答案必须由 gitea-mcp 的 Issue/PR comment 工具发送;cc-web 记录
|
||||||
|
MCP 返回的 `commentId` 或工具调用结果,并通过 Gitea 查询确认实际存在。
|
||||||
|
- 确认必须同时匹配 `resourceKey`、`taskId`、`turnId`(或等价不可见标记)和
|
||||||
|
Bot 作者,避免把旧评论当作本轮回执。
|
||||||
|
- 仅在确认 Gitea 评论查询结果为“缺失”时,发起一次“只补发回执、不重复修改”的
|
||||||
|
隐藏 turn;补发仍缺失才以 Bot Token 经 Gitea REST 发布最终正文。查询结果为
|
||||||
|
`unknown` 时不自动重试或覆盖,任务进入 `failed_reply`,由终态兜底回执告知用户,
|
||||||
|
避免把已经成功落库的 MCP 回复重复发布。
|
||||||
|
|
||||||
|
### FR-07 重试、恢复与状态一致性
|
||||||
|
|
||||||
|
- 网络/进程级暂时错误使用持久 `retry_wait` 和指数退避;执行 turn 因重启被
|
||||||
|
中断时最多自动重试一次,禁止无界重试。
|
||||||
|
- `queued` 重启后恢复调度;`waiting_user` 保留原会话等待新评论;`running`
|
||||||
|
恢复为 `retry_wait` 并带 `interrupted_by_restart` 原因;终态不重新执行。
|
||||||
|
- 所有状态变更、评论发送、MCP 工具调用和重试都追加审计事件,状态写入遵循
|
||||||
|
版本号/单调序列,防止旧事件覆盖新状态。
|
||||||
|
|
||||||
|
### FR-08 运维控制与审计
|
||||||
|
|
||||||
|
- 全局 `pause/resume`:暂停只阻止新任务领取,已运行任务继续或由管理员另行中止。
|
||||||
|
- 仓库 `disable/enable`:停用后拒绝该仓库的新触发并保留历史;正在运行的任务
|
||||||
|
不强制杀死。
|
||||||
|
- `cancel queued`、`abort running turn` 必须幂等,响应包含当前状态和操作者。
|
||||||
|
- 审计事件至少包含 `eventId、taskId、sessionKey、actor、action、fromState、
|
||||||
|
toState、deliveryId、turnId、commentId、errorCode、timestamp`;支持按仓库、
|
||||||
|
资源、任务和时间范围查询。
|
||||||
|
|
||||||
|
## 6. 状态机
|
||||||
|
|
||||||
|
### 6.1 任务状态
|
||||||
|
|
||||||
|
| 状态 | 含义 | 可转移 |
|
||||||
|
|---|---|---|
|
||||||
|
| `received` | 已验签并完成事件规范化 | `queued`、`ignored`、`duplicate`、`rejected` |
|
||||||
|
| `queued` | 等待仓库锁和全局槽位 | `preparing`、`cancelled` |
|
||||||
|
| `preparing` | 正在登记仓库、clone/fetch、构造线程 | `running`、`blocked_workspace`、`retry_wait`、`failed` |
|
||||||
|
| `running` | Codex App turn 执行中 | `waiting_user`、`verifying_reply`、`retry_wait`、`aborting`、`failed` |
|
||||||
|
| `waiting_user` | Agent 需要用户补充,保留会话 | `queued`(新评论)、`cancelled`、`failed` |
|
||||||
|
| `verifying_reply` | 已收到 turn 完成,等待确认最终评论 | `succeeded`、`retry_wait`、`failed_reply` |
|
||||||
|
| `retry_wait` | 到达退避时间,等待有限次重试 | `preparing`、`running`、`verifying_reply`、`failed` |
|
||||||
|
| `blocked_workspace` | 工作区脏/冲突/权限问题 | `queued`(管理员解除后)、`cancelled` |
|
||||||
|
| `aborting` | 已请求取消,等待 Codex App 确认 | `aborted`、`failed` |
|
||||||
|
| `succeeded` | 最终回帖已确认 | 终态 |
|
||||||
|
| `succeeded_with_rest_fallback` | 使用 REST 兜底回帖成功 | 终态 |
|
||||||
|
| `failed` / `failed_reply` | 执行或回帖不可恢复失败 | 终态 |
|
||||||
|
| `cancelled` / `aborted` | 排队取消或运行中止 | 终态 |
|
||||||
|
| `ignored` / `duplicate` / `rejected` | 未触发、重复或安全拒绝 | 终态 |
|
||||||
|
|
||||||
|
### 6.2 状态不变量
|
||||||
|
|
||||||
|
1. 只有 `preparing/running/aborting` 持有仓库锁;同一 `repoKey` 同时最多一个
|
||||||
|
运行中任务。
|
||||||
|
2. 终态任务不得创建新 turn、状态评论或重试;重复 Webhook 只能返回原任务。
|
||||||
|
3. `verifying_reply` 必须引用本轮 `turnId`;`failed_reply` 不代表代码执行必然
|
||||||
|
失败,必须在审计中区分 `executionError` 与 `replyError`。
|
||||||
|
4. `waiting_user` 的新评论创建新 `taskId`,但复用同一 `sessionKey/threadId`。
|
||||||
|
|
||||||
|
## 7. 非功能需求与风险
|
||||||
|
|
||||||
|
| 类别 | 要求 |
|
||||||
|
|---|---|
|
||||||
|
| 安全 | HMAC 常量时间校验、凭据隔离、Bot 自触发抑制、敏感字段脱敏 |
|
||||||
|
| 一致性 | delivery/task/session/turn/comment 均有唯一键;状态变更可重放且幂等 |
|
||||||
|
| 可恢复性 | 进程重启不丢队列、会话映射和回帖确认上下文 |
|
||||||
|
| 可观测性 | 结构化日志 + 审计事件 + task/session/turn 关联 ID |
|
||||||
|
| 性能 | Webhook 验签与入队不等待 Agent 完成;并发只受仓库锁约束 |
|
||||||
|
| 数据保留 | 任务/会话/审计至少保留到管理员显式清理策略生效;工作区持久保留 |
|
||||||
|
| 风险 | gitea-mcp 全写权限意味着 Bot 能修改代码和 Gitea 内容;通过 HMAC、暂停、
|
||||||
|
中止、审计和脏工作区保护降低风险,不声称实现细粒度授权 |
|
||||||
|
|
||||||
|
## 8. 验收标准与需求覆盖矩阵
|
||||||
|
|
||||||
|
每个验收项都应能在集成测试、协议 mock 或只读审计中独立验证。
|
||||||
|
|
||||||
|
| ID | 验收标准 | 覆盖需求/设计 |
|
||||||
|
|---|---|---|
|
||||||
|
| AC-01 | 合法 Issue 普通评论含 `@ccweb-bot` 创建任务;无 mention、非评论或 Bot 自评论不创建任务 | FR-01 |
|
||||||
|
| AC-02 | HMAC 错误返回 401 且无业务写入;相同 delivery 重放不增加任务数 | FR-02 |
|
||||||
|
| AC-03 | 首次合法触发自动登记仓库并 clone;再次触发只 fetch;脏工作区不被 reset/覆盖 | FR-03 |
|
||||||
|
| AC-04 | 同仓库两个任务严格串行;两个仓库可并行;不额外施加全局并发上限 | FR-04 |
|
||||||
|
| AC-05 | 同一 Issue/PR 多条评论复用一个 `threadId`,每条任务有唯一 `turnId` | FR-05 |
|
||||||
|
| AC-06 | Codex App 运行时固定 `codexapp+yolo`;官方 gitea-mcp 以线程级 stdio 配置启动 | FR-05 |
|
||||||
|
| AC-07 | 合法任务仅有一条“已收到”起始回执;成功正文由 MCP/REST 兜底承担,失败或控制终态最多一条结果回执,Bot 评论不会回触发 | FR-06 |
|
||||||
|
| AC-08 | MCP 最终回帖能按 `taskId+turnId+resourceKey` 确认;缺失只补发一次,随后 REST 兜底 | FR-06 |
|
||||||
|
| AC-09 | 重启后 queued 继续、waiting_user 保留、running 最多重试一次;终态不重跑 | FR-07 |
|
||||||
|
| AC-10 | pause 阻止新领取;仓库 disable 拒绝新任务;cancel/abort 幂等且释放相应资源 | FR-08 |
|
||||||
|
| AC-11 | 审计可按 task/session/repo 查询,包含状态迁移、delivery、turn、comment 和错误字段 | FR-08 |
|
||||||
|
| AC-12 | PRD 与 ARCHITECTURE 的状态枚举、字段名、默认并发、回帖错误语义完全一致 | 文档一致性 |
|
||||||
|
| AC-13 | 生产挂接包含 `server.js` 路由、`lib/gitea-workflow-*` 核心模块、管理页与离线回归;不新增第三方依赖 | 交付约束 |
|
||||||
|
|
||||||
|
## 9. 交付边界与后续演进
|
||||||
|
|
||||||
|
当前仓库已提供 MVP 实现:`POST /api/gitea/webhook`、持久任务/队列、工作区维护、
|
||||||
|
Codex App 线程级 gitea-mcp 注入、回帖确认/兜底、管理 API/UI 和离线回归。实现与
|
||||||
|
测试必须继续以本 PRD 的 FR/AC 编号作为审计索引。
|
||||||
|
|
||||||
|
后续可独立评估:多实例、细粒度授权、审批门、工作区隔离容器、任务预算、
|
||||||
|
SSE/前端实时镜像和更多 Forge 适配;这些不改变 MVP 的 `sessionKey`、幂等和回帖
|
||||||
|
确认约束。
|
||||||
155
docs/gitea-workflow/TESTING.md
Normal file
155
docs/gitea-workflow/TESTING.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Gitea Webhook Agent 测试设计
|
||||||
|
|
||||||
|
> 本文与 [PRD.md](./PRD.md) 的 FR/AC 以及
|
||||||
|
> [ARCHITECTURE.md](./ARCHITECTURE.md) 的字段、状态和错误矩阵对齐。独立协议
|
||||||
|
> 回归入口为:
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> timeout 60s node scripts/gitea-webhook-regression.js
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> 脚本只使用 Node 内置模块和 `fixtures/gitea-workflow/*.json`,不启动
|
||||||
|
> `server.js`、不访问网络、不写业务 Store,适合实现落地前后的离线验收。
|
||||||
|
|
||||||
|
## 1. 测试金字塔
|
||||||
|
|
||||||
|
| 层级 | 目标 | 本轮入口 |
|
||||||
|
|---|---|---|
|
||||||
|
| 契约/纯函数 | HMAC、事件规范化、mention/Bot 过滤、幂等键 | `gitea-webhook-regression.js` |
|
||||||
|
| 状态模型 | queued/running/retry_wait/终态恢复和有限重试 | 同上 + 实现方状态机单测 |
|
||||||
|
| 协议集成 | HTTP header、Gitea REST、Codex JSON-RPC、MCP stdio | mock server/adapter 集成测试 |
|
||||||
|
| 端到端 | 真实 Gitea 测试仓库到最终评论 | staging 手工/CI smoke,不纳入离线脚本 |
|
||||||
|
| 运维验收 | 重启、备份恢复、pause/disable/abort、告警脱敏 | staging runbook 演练 |
|
||||||
|
|
||||||
|
离线脚本是最小门禁;当前仓库已提供以下专项门禁:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/gitea-webhook-regression.js
|
||||||
|
node scripts/gitea-webhook-workspace-unit.js
|
||||||
|
node scripts/gitea-workflow-codex-unit.js
|
||||||
|
node scripts/gitea-workflow-core-unit.js
|
||||||
|
node scripts/gitea-workflow-management-unit.js
|
||||||
|
```
|
||||||
|
|
||||||
|
仍需在 staging 用真实 Gitea 验证出站回帖确认;纯函数通过不能替代真实凭据和
|
||||||
|
网络链路验收。
|
||||||
|
|
||||||
|
## 2. Fixtures 约定
|
||||||
|
|
||||||
|
所有 fixture 均为 JSON,放在 `fixtures/gitea-workflow/`,不含真实 Secret、Token、
|
||||||
|
源码或生产 URL。字段约定:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "valid-issue-comment",
|
||||||
|
"headers": {
|
||||||
|
"x-gitea-event": "issue_comment",
|
||||||
|
"x-gitea-delivery": "delivery-001"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"repository": {"owner": "acme", "name": "widget"},
|
||||||
|
"issue": {"number": 42},
|
||||||
|
"comment": {"id": 101, "body": "@ccweb-bot 请修复"},
|
||||||
|
"sender": {"login": "alice", "is_bot": false}
|
||||||
|
},
|
||||||
|
"expected": {"accepted": true, "resourceKind": "issue"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本运行时用固定测试 Secret 计算签名;签名不落盘。需要拒绝签名时复制
|
||||||
|
payload 后篡改 raw body 或签名头。fixture 变更必须同步场景 ID 和预期结果。
|
||||||
|
|
||||||
|
## 3. 需求覆盖矩阵
|
||||||
|
|
||||||
|
| 场景 ID | 验证内容 | Fixture/断言 | 对应 AC |
|
||||||
|
|---|---|---|---|
|
||||||
|
| WH-01 | 合法 Issue 评论 mention 触发,规范化 owner/repo/number/comment | `valid-issue-comment.json` | AC-01 |
|
||||||
|
| WH-02 | 合法 PR 评论进入 `pull_request` 资源,保留 base/head | `valid-pr-comment.json` | AC-01/03 |
|
||||||
|
| WH-03 | HMAC 缺失/错误不写业务任务,常量时间比较 | `valid-issue-comment.json` 篡改签名 | AC-02 |
|
||||||
|
| WH-04 | 相同 `instanceId:deliveryId` 重放返回 duplicate,不增 task | valid fixture 两次提交 | AC-02 |
|
||||||
|
| WH-05 | 无 mention、非评论事件、Bot 自评论、停用仓库均 ignored/rejected | `ignored-events.json`、`bot-self-comment.json` | AC-01/10 |
|
||||||
|
| Q-01 | 重启后 queued 保留;running 变 `retry_wait(interrupted_by_restart)` | `queue-recovery.json` | AC-09 |
|
||||||
|
| Q-02 | 已有重试次数达到上限后进入 failed;waiting_user/终态不重跑 | 同上 | AC-09 |
|
||||||
|
| Q-03 | 同仓库运行锁互斥;跨仓库可以并行,不设置额外全局活动上限 | 脚本内最小状态快照 | AC-04 |
|
||||||
|
| R-00 | 合法触发仅产生一条 `giteabot: 已收到` 起始回执;成功路径不产生运行中/等待/已完成状态评论 | server.js 回执策略断言 | AC-07 |
|
||||||
|
| R-01 | MCP 回帖成功但查询缺失只创建一个 reply_retry | `reply-failure.json` | AC-08 |
|
||||||
|
| R-02 | 补发仍失败才 REST 兜底,结果为 succeeded_with_rest_fallback | 同上 | AC-08 |
|
||||||
|
| MCP-01 | `thread/start.config.mcp_servers.gitea` 为线程级 stdio 配置 | `mcp-thread-config.json` | AC-06 |
|
||||||
|
| MCP-02 | collaborationMode 字段放 settings,顶层无 model/effort 重复字段 | 同上 | AC-06 |
|
||||||
|
| MCP-03 | gitea-mcp 命令缺失、进程启动即退、initialize 返回 error、握手超时分别映射稳定错误码 | `gitea-mcp-probe-unit.js` + mock stdio fixture | AC-06/07 |
|
||||||
|
| MCP-04 | 预检失败后任务只产生一条 `giteabot:` 失败终态回执;线程级注入仍保留且不自动安装二进制 | `gitea-mcp-probe-unit.js` 源码守门 | AC-06/07/11 |
|
||||||
|
| SEC-01 | Secret/token 不进入日志、评论、clone URL 或 fixture | 全部 fixture + 输出扫描 | AC-02/11 |
|
||||||
|
| SEC-02 | 路径穿越、任意命令、越权控制参数被拒绝 | 实现方安全单测 | AC-03/10/11 |
|
||||||
|
|
||||||
|
## 4. 状态、恢复与并发测试
|
||||||
|
|
||||||
|
实现方 Store 测试必须验证以下不变量:
|
||||||
|
|
||||||
|
1. `deliveryKey`、`sessionKey`、`taskId+turnId`、`commentKey` 唯一;重复请求
|
||||||
|
返回原记录且不创建新 turn/comment。
|
||||||
|
2. 同一 `repoKey` 同时最多一个 `preparing/running/aborting`;不同 `repoKey`
|
||||||
|
可以并行;pause 只阻止领取,不强制终止运行中任务。
|
||||||
|
3. 重启扫描用 `stateVersion`/`leaseId` CAS,旧 worker 不能覆盖新租约;
|
||||||
|
`running` 最多自动恢复一次,超过上限进入 `failed`。
|
||||||
|
4. `waiting_user` 保留 thread,新评论创建新 task 但复用 `sessionKey/threadId`;
|
||||||
|
所有终态不再自动执行。
|
||||||
|
5. `blocked_workspace` 不执行 reset/clean;管理员解除后才可重新入队。
|
||||||
|
|
||||||
|
故障注入至少覆盖进程在“intent 已落库、外部调用未完成”和“外部返回成功、
|
||||||
|
结果未确认”两个窗口崩溃,恢复后必须通过幂等键继续,而非盲目重复修改。
|
||||||
|
|
||||||
|
## 5. 回帖重试测试
|
||||||
|
|
||||||
|
按以下顺序模拟:
|
||||||
|
|
||||||
|
1. MCP comment 返回 accepted,但 Gitea 查询无匹配标记:状态进入
|
||||||
|
`verifying_reply`,只允许一个 `reply_retry` turn。
|
||||||
|
2. `reply_retry` 仍无评论:调用一次 REST 兜底,body 复用 `taskId/turnId/resourceKey`
|
||||||
|
标记;查询成功为 `succeeded_with_rest_fallback`。
|
||||||
|
3. REST 返回 2xx 但查询仍为空:状态必须是 `failed_reply`,不能虚报成功,也不得
|
||||||
|
无限重试。
|
||||||
|
4. 原 MCP 评论已确认后重复 webhook/重启:不得再发送起始、状态、最终或兜底评论。
|
||||||
|
|
||||||
|
测试日志只输出 `taskId/turnId/commentId/errorCode`,不得输出正文中的 Secret 或
|
||||||
|
完整 Authorization。
|
||||||
|
|
||||||
|
## 6. MCP/JSON-RPC 协议测试
|
||||||
|
|
||||||
|
mock app-server 必须断言初始化顺序为:`initialize` → `initialized` → best-effort
|
||||||
|
`experimentalFeature/enablement/set`、`collaborationMode/list` → `thread/start`。
|
||||||
|
能力探测失败只记录降级,不阻断启动。
|
||||||
|
|
||||||
|
另需运行 `node scripts/gitea-mcp-probe-unit.js`,它会以独立 mock stdio 进程验证
|
||||||
|
gitea-mcp 的 initialize 预检,不需要真实 Gitea、Token 或 Docker。
|
||||||
|
|
||||||
|
`thread/start` 断言:
|
||||||
|
|
||||||
|
- `config.mcp_servers.gitea` 每个线程单独携带 host/token 引用和 stdio command;
|
||||||
|
- `CC_WEB_SOURCE_SESSION_ID` 等来源上下文不能只存在进程环境;
|
||||||
|
- 使用 collaboration mode 时,`model`、`reasoning_effort`、
|
||||||
|
`developer_instructions` 只在 `collaborationMode.settings`,顶层不存在重复
|
||||||
|
`model`/`effort`;拒绝 collaborationMode 时可降级普通 turn。
|
||||||
|
|
||||||
|
## 7. 安全与运维验收
|
||||||
|
|
||||||
|
- 验签使用 raw body + HMAC-SHA256 + 常量时间比较;缺失/错误签名 401 且无业务写入。
|
||||||
|
- 事件正文、Issue 描述、代码和 PR 内容均视为不可信 prompt;不能改变系统约束、
|
||||||
|
读取 Secret、关闭审计或注入任意命令。
|
||||||
|
- Bot 自评论、停用仓库、非评论事件和无 mention 事件不启动 Agent;每个决定有
|
||||||
|
审计 action/errorCode。
|
||||||
|
- clone/fetch 使用不含 Token 的 remote URL;工作区路径必须在允许根目录内,
|
||||||
|
路径穿越和脏目录覆盖均拒绝。
|
||||||
|
- 控制 API 需要管理员身份、reason 和 idempotency key;取消/中止幂等。
|
||||||
|
- 日志和审计脱敏检查覆盖 Secret、Bot Token、Authorization、MCP args/env。
|
||||||
|
|
||||||
|
## 8. 执行清单
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --check scripts/gitea-webhook-regression.js
|
||||||
|
timeout 60s node scripts/gitea-webhook-regression.js
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
CI 失败时保留 stdout/stderr、fixture 名称和场景 ID;不得重试隐藏不稳定失败。
|
||||||
|
真实 Gitea staging 验收完成后,应将 delivery/task/thread/turn/comment 关联链和
|
||||||
|
最终状态写入审计报告,作为发布批准依据。
|
||||||
18
findings.md
18
findings.md
@@ -70,3 +70,21 @@
|
|||||||
- 多数 `.sh` hook 脚本没有 `+x`,但 `.codex/hooks.json` 使用 `sh script` / `python3 script` 调用,因此执行位不是当前阻断点。
|
- 多数 `.sh` hook 脚本没有 `+x`,但 `.codex/hooks.json` 使用 `sh script` / `python3 script` 调用,因此执行位不是当前阻断点。
|
||||||
- 与断点 4 合并后的结论:本地脚本没坏,当前阻断点在 Codex command hook trust,7 条项目 command hook 均为 `untrusted`,所以正常 app-server 路径会跳过它们。
|
- 与断点 4 合并后的结论:本地脚本没坏,当前阻断点在 Codex command hook trust,7 条项目 command hook 均为 `untrusted`,所以正常 app-server 路径会跳过它们。
|
||||||
- marker 方案仍可用于 trust 后二次验证;但在 hooks 未 trust 前,marker 不出现只能证明 trust 拦截,不再是定位根因的必要步骤。
|
- marker 方案仍可用于 trust 后二次验证;但在 hooks 未 trust 前,marker 不出现只能证明 trust 拦截,不再是定位根因的必要步骤。
|
||||||
|
|
||||||
|
## 2026-08-23 CentOS 7 发布包复核
|
||||||
|
|
||||||
|
- 当前 `package.json` 版本为 `1.2.11`,构建入口为 `scripts/build-single-exe.js`。
|
||||||
|
- `dist-exe/cc-web-bun-linux-x64-baseline.tar.gz` 与其中二进制最后修改时间均为 `2026-08-21 16:57`;源码 `server.js`、`public/app.js` 随后已有变更,不能确认旧包包含这些变更。
|
||||||
|
- PATH 中没有 `bun`,但 `/tmp/ccweb-bun.vyBprA/`、`/tmp/ccweb-bun.35Wj3R/` 等目录存在 `@oven/bun-linux-x64-baseline/bin/bun`,可按发布技能通过 `BUN_BIN` 复用。
|
||||||
|
- 发布脚本默认目标为 `bun-linux-x64-baseline`,会清理并重建 `dist-exe/bun-linux-x64-baseline/`,随后生成同名 tar.gz。
|
||||||
|
- 2026-08-23 重建成功:目录二进制 SHA-256 为 `28f2c2b99ef66cb523feef171c8c2438071e724629fed44f348734b982d4a4e7`;tar.gz SHA-256 为 `d8a9de5813a6f4c8b27f05565926faa9267e9070df4241387568169d97ce5cbe`。
|
||||||
|
- `tar -xOf` 提取归档内的 `bun-linux-x64-baseline/cc-web` 后,其 SHA-256 与目录二进制完全一致;包内 `package.json` 版本为 `1.2.11`。
|
||||||
|
- 从 `./dist-exe/bun-linux-x64-baseline/cc-web` 直接启动本机等价验证通过:端口 18082 可访问,启动日志包含 `CC-Web server listening`,运行进程 `/proc/<pid>/exe` 指向该发布目录且摘要一致。
|
||||||
|
|
||||||
|
## 2026-08-23 start.sh 启动回退修复
|
||||||
|
|
||||||
|
- `start.sh` 默认仍走原有 Node/PM2 源码启动;只在 `CC_WEB_START_MODE=auto`、源码启动失败且系统识别为 CentOS 7 时回退。
|
||||||
|
- 启动前会释放解析出的监听端口,默认端口为 8002;源码路径和 single-exe 回退路径都会再次确认端口已释放。
|
||||||
|
- single-exe 通过 `CC_WEB_APP_DIR` 指向项目根目录,复用根目录 `config/auth.json`、sessions/logs 和 `.env`,不会强制设置 `CC_WEB_PASSWORD`。
|
||||||
|
- 回退前会比较归档内二进制 SHA-256,发现 `dist-exe/bun-linux-x64-baseline/cc-web` 过旧时自动从 `dist-exe/cc-web-bun-linux-x64-baseline.tar.gz` 刷新。
|
||||||
|
- 回归验证覆盖:源码/PM2 端口 18084 启动、single-exe 复用 auth.json、占用端口自动关闭后 single-exe 启动;均通过。
|
||||||
|
|||||||
11
fixtures/gitea-workflow/bot-self-comment.json
Normal file
11
fixtures/gitea-workflow/bot-self-comment.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "bot-self-comment",
|
||||||
|
"headers": {"x-gitea-event": "issue_comment", "x-gitea-delivery": "delivery-bot-001"},
|
||||||
|
"payload": {
|
||||||
|
"repository": {"owner": "acme", "name": "widget"},
|
||||||
|
"issue": {"number": 42, "pull_request": false},
|
||||||
|
"comment": {"id": 303, "body": "@ccweb-bot 状态更新", "user": {"login": "ccweb-bot", "type": "Bot"}},
|
||||||
|
"sender": {"login": "ccweb-bot", "is_bot": true}
|
||||||
|
},
|
||||||
|
"expected": {"accepted": false, "reason": "bot_self_comment"}
|
||||||
|
}
|
||||||
19
fixtures/gitea-workflow/ignored-events.json
Normal file
19
fixtures/gitea-workflow/ignored-events.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "push-event",
|
||||||
|
"headers": {"x-gitea-event": "push", "x-gitea-delivery": "delivery-push-001"},
|
||||||
|
"payload": {"repository": {"owner": "acme", "name": "widget"}, "sender": {"login": "alice"}},
|
||||||
|
"expected": {"accepted": false, "reason": "unsupported_event"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "comment-without-mention",
|
||||||
|
"headers": {"x-gitea-event": "issue_comment", "x-gitea-delivery": "delivery-no-mention-001"},
|
||||||
|
"payload": {
|
||||||
|
"repository": {"owner": "acme", "name": "widget"},
|
||||||
|
"issue": {"number": 42, "pull_request": false},
|
||||||
|
"comment": {"id": 404, "body": "请看看这个问题", "user": {"login": "alice"}},
|
||||||
|
"sender": {"login": "alice", "is_bot": false}
|
||||||
|
},
|
||||||
|
"expected": {"accepted": false, "reason": "missing_mention"}
|
||||||
|
}
|
||||||
|
]
|
||||||
16
fixtures/gitea-workflow/mcp-thread-config.json
Normal file
16
fixtures/gitea-workflow/mcp-thread-config.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"cwd": "/var/lib/ccweb/workspaces/default/acme/widget",
|
||||||
|
"mcpServer": {
|
||||||
|
"name": "gitea",
|
||||||
|
"command": "gitea-mcp",
|
||||||
|
"args": ["-t", "stdio", "-H", "https://gitea.example"],
|
||||||
|
"env": {"GITEA_ACCESS_TOKEN": "secret-ref:gitea-bot-token"}
|
||||||
|
},
|
||||||
|
"collaborationMode": {
|
||||||
|
"settings": {
|
||||||
|
"model": "configured-model",
|
||||||
|
"reasoning_effort": "medium",
|
||||||
|
"developer_instructions": "只在授权工作区内操作并回帖"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
fixtures/gitea-workflow/mock-gitea-mcp.js
Normal file
28
fixtures/gitea-workflow/mock-gitea-mcp.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 仅用于离线回归:模拟官方 gitea-mcp 的 JSONL initialize 行为。
|
||||||
|
const readline = require('readline');
|
||||||
|
|
||||||
|
const mode = String(process.env.GITEA_MCP_FIXTURE_MODE || 'success');
|
||||||
|
const reader = readline.createInterface({ input: process.stdin });
|
||||||
|
reader.on('line', (line) => {
|
||||||
|
let request;
|
||||||
|
try { request = JSON.parse(line); } catch { return; }
|
||||||
|
if (request.method !== 'initialize') return;
|
||||||
|
if (mode === 'timeout') return;
|
||||||
|
if (mode === 'exit') process.exit(17);
|
||||||
|
if (mode === 'error') {
|
||||||
|
process.stdout.write(`${JSON.stringify({
|
||||||
|
jsonrpc: '2.0', id: request.id, error: { code: -32000, message: 'fixture handshake failed' },
|
||||||
|
})}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.stdout.write(`${JSON.stringify({
|
||||||
|
jsonrpc: '2.0', id: request.id,
|
||||||
|
result: {
|
||||||
|
protocolVersion: '2024-11-05',
|
||||||
|
capabilities: { tools: {} },
|
||||||
|
serverInfo: { name: 'fixture-gitea-mcp', version: '0.0.0' },
|
||||||
|
},
|
||||||
|
})}\n`);
|
||||||
|
});
|
||||||
9
fixtures/gitea-workflow/queue-recovery.json
Normal file
9
fixtures/gitea-workflow/queue-recovery.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"tasks": [
|
||||||
|
{"taskId": "task-queued", "state": "queued", "attempt": 0},
|
||||||
|
{"taskId": "task-running", "state": "running", "attempt": 0},
|
||||||
|
{"taskId": "task-waiting", "state": "waiting_user", "attempt": 0},
|
||||||
|
{"taskId": "task-terminal", "state": "succeeded", "attempt": 0},
|
||||||
|
{"taskId": "task-exhausted", "state": "running", "attempt": 1}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
fixtures/gitea-workflow/reply-failure.json
Normal file
9
fixtures/gitea-workflow/reply-failure.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"taskId": "task-reply-001",
|
||||||
|
"turnId": "turn-001",
|
||||||
|
"resourceKey": "default:acme/widget:issue:42",
|
||||||
|
"mcpAccepted": true,
|
||||||
|
"mcpCommentVisible": false,
|
||||||
|
"retryCommentVisible": false,
|
||||||
|
"restFallbackVisible": true
|
||||||
|
}
|
||||||
19
fixtures/gitea-workflow/valid-issue-comment.json
Normal file
19
fixtures/gitea-workflow/valid-issue-comment.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "valid-issue-comment",
|
||||||
|
"headers": {
|
||||||
|
"x-gitea-event": "issue_comment",
|
||||||
|
"x-gitea-delivery": "delivery-issue-001"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"repository": {
|
||||||
|
"owner": "acme",
|
||||||
|
"name": "widget",
|
||||||
|
"clone_url": "https://gitea.example/acme/widget.git",
|
||||||
|
"default_branch": "main"
|
||||||
|
},
|
||||||
|
"issue": {"number": 42, "pull_request": false},
|
||||||
|
"comment": {"id": 101, "body": "@ccweb-bot 请修复登录校验", "user": {"login": "alice"}},
|
||||||
|
"sender": {"login": "alice", "is_bot": false}
|
||||||
|
},
|
||||||
|
"expected": {"accepted": true, "resourceKind": "issue", "instruction": "请修复登录校验"}
|
||||||
|
}
|
||||||
23
fixtures/gitea-workflow/valid-pr-comment.json
Normal file
23
fixtures/gitea-workflow/valid-pr-comment.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "valid-pr-comment",
|
||||||
|
"headers": {
|
||||||
|
"x-gitea-event": "pull_request_comment",
|
||||||
|
"x-gitea-delivery": "delivery-pr-001"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"repository": {
|
||||||
|
"owner": "acme",
|
||||||
|
"name": "widget",
|
||||||
|
"clone_url": "https://gitea.example/acme/widget.git",
|
||||||
|
"default_branch": "main"
|
||||||
|
},
|
||||||
|
"pull_request": {
|
||||||
|
"number": 7,
|
||||||
|
"base": {"ref": "main"},
|
||||||
|
"head": {"ref": "feature/login", "repo": {"clone_url": "https://gitea.example/alice/widget.git"}}
|
||||||
|
},
|
||||||
|
"comment": {"id": 202, "body": "请继续处理 @ccweb-bot 的 review 意见", "user": {"login": "bob"}},
|
||||||
|
"sender": {"login": "bob", "is_bot": false}
|
||||||
|
},
|
||||||
|
"expected": {"accepted": true, "resourceKind": "pull_request", "base": "main", "head": "feature/login"}
|
||||||
|
}
|
||||||
@@ -37,6 +37,16 @@ function createAgentRuntime(deps) {
|
|||||||
256 * 1024,
|
256 * 1024,
|
||||||
{ min: 4096 },
|
{ min: 4096 },
|
||||||
);
|
);
|
||||||
|
const CCWEB_MCP_STARTUP_TIMEOUT_SEC = readRuntimePositiveIntEnv(
|
||||||
|
'CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC',
|
||||||
|
30,
|
||||||
|
{ min: 10, max: 300 },
|
||||||
|
);
|
||||||
|
const CCWEB_MCP_TOOL_TIMEOUT_SEC = readRuntimePositiveIntEnv(
|
||||||
|
'CC_WEB_CODEX_MCP_TOOL_TIMEOUT_SEC',
|
||||||
|
60,
|
||||||
|
{ min: 10, max: 600 },
|
||||||
|
);
|
||||||
const RUNTIME_TRUNCATED_HEAD = '[cc-web: 前文过长,已保留尾部以保护服务稳定性]\n';
|
const RUNTIME_TRUNCATED_HEAD = '[cc-web: 前文过长,已保留尾部以保护服务稳定性]\n';
|
||||||
|
|
||||||
function keepTail(value, maxLen) {
|
function keepTail(value, maxLen) {
|
||||||
@@ -99,8 +109,8 @@ function createAgentRuntime(deps) {
|
|||||||
'-c', `mcp_servers.ccweb.command=${tomlString(nodePath || 'node')}`,
|
'-c', `mcp_servers.ccweb.command=${tomlString(nodePath || 'node')}`,
|
||||||
'-c', `mcp_servers.ccweb.args=${tomlStringArray(serverArgs)}`,
|
'-c', `mcp_servers.ccweb.args=${tomlStringArray(serverArgs)}`,
|
||||||
'-c', `mcp_servers.ccweb.env_vars=${tomlStringArray(envVars)}`,
|
'-c', `mcp_servers.ccweb.env_vars=${tomlStringArray(envVars)}`,
|
||||||
'-c', 'mcp_servers.ccweb.startup_timeout_sec=10',
|
'-c', `mcp_servers.ccweb.startup_timeout_sec=${CCWEB_MCP_STARTUP_TIMEOUT_SEC}`,
|
||||||
'-c', 'mcp_servers.ccweb.tool_timeout_sec=60'
|
'-c', `mcp_servers.ccweb.tool_timeout_sec=${CCWEB_MCP_TOOL_TIMEOUT_SEC}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const http = require('http');
|
|||||||
const https = require('https');
|
const https = require('https');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { SCRIPT_TOOL_DEFINITIONS } = require('./javascript-session-runtime');
|
||||||
|
|
||||||
const SERVER_INFO = {
|
const SERVER_INFO = {
|
||||||
name: 'ccweb',
|
name: 'ccweb',
|
||||||
@@ -15,7 +16,7 @@ const CCWEB_REPLY_MODES = Object.freeze({
|
|||||||
ONE_WAY: 'one_way',
|
ONE_WAY: 'one_way',
|
||||||
RETURN_AND_CONTINUE: 'return_and_continue',
|
RETURN_AND_CONTINUE: 'return_and_continue',
|
||||||
});
|
});
|
||||||
const CCWEB_SEND_MESSAGE_DESCRIPTION = '向指定 ccweb 对话发送一条消息,并以“来自某对话”的气泡在目标对话中展示。必须填写 replyMode:仅当来源不需要目标结果时使用 one_way;涉及分析、实现、测试、验收、完成后汇报或来源后续依赖目标结果时,必须使用 return_and_continue。工具调用会立即返回,不阻塞也不等待目标对话完成。';
|
const CCWEB_SEND_MESSAGE_DESCRIPTION = '向指定 ccweb 对话发送一条消息,并以“来自某对话”的气泡在目标对话中展示。必须填写 replyMode:仅当来源不需要目标结果时使用 one_way;涉及分析、实现、测试、验收、完成后汇报或来源后续依赖目标结果时必须使用 return_and_continue。两种模式的工具调用都会立即返回,不阻塞也不等待目标对话完成;return_and_continue 只表示目标完成后由系统异步回传并触发来源继续运行,不表示当前调用需要等待。调用后不要使用 sleep、不要轮询等待,也不要反复查询 pending reply;继续当前工作或结束本轮即可。';
|
||||||
const CODEX_APP_COMMUNICATION_TOOL_NAMES = new Set([
|
const CODEX_APP_COMMUNICATION_TOOL_NAMES = new Set([
|
||||||
'ccweb_list_conversations',
|
'ccweb_list_conversations',
|
||||||
'ccweb_list_user_inputs',
|
'ccweb_list_user_inputs',
|
||||||
@@ -28,6 +29,13 @@ const CODEX_APP_COMMUNICATION_TOOL_NAMES = new Set([
|
|||||||
const HIDDEN_CALLABLE_TOOL_NAMES = new Set([
|
const HIDDEN_CALLABLE_TOOL_NAMES = new Set([
|
||||||
'ccweb_request_reply',
|
'ccweb_request_reply',
|
||||||
'ccweb_task_update',
|
'ccweb_task_update',
|
||||||
|
'ccweb_script_get_current_conversation_id',
|
||||||
|
'ccweb_script_create_conversation',
|
||||||
|
'ccweb_script_send_message',
|
||||||
|
'ccweb_script_select_semantic_branch',
|
||||||
|
'ccweb_script_get_last_message',
|
||||||
|
'ccweb_script_get_conversation_status',
|
||||||
|
'ccweb_script_get_child_conversation_ids',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const TOOLS = [
|
const TOOLS = [
|
||||||
@@ -120,7 +128,7 @@ const TOOLS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ccweb_create_conversation',
|
name: 'ccweb_create_conversation',
|
||||||
description: '创建一个新的 ccweb 持久对话。Agent 固定继承来源对话,不作为参数指定;只用于需要在会话列表中长期追踪、后续可继续对话的工作流;一次性并行研究应优先使用子代能力。',
|
description: '创建一个新的 ccweb 持久对话。Agent 固定继承来源对话,不作为参数指定;只用于需要在会话列表中长期追踪、后续可继续对话的工作流;一次性并行研究应优先使用子代能力。工具调用会立即返回。启用 requestReply 后,默认由系统异步回传结果;除非用户明确要求轮询,否则不主动轮询等待。',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -144,7 +152,7 @@ const TOOLS = [
|
|||||||
},
|
},
|
||||||
requestReply: {
|
requestReply: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: '可选。若为 true,会在新对话完成本轮输出后把回复写回来源对话,并继续触发来源对话运行。默认 false。',
|
description: '可选。控制是否自动回传结果,默认 false:结果留在新对话,不自动回传;true:新对话完成本轮输出后,系统自动把回复写回来源对话并触发其继续运行,必须同时提供 initialMessage。两种情况下本调用都立即返回。启用回传后,除非用户明确要求轮询,否则由系统异步回传结果,无需主动轮询。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -167,7 +175,7 @@ const TOOLS = [
|
|||||||
replyMode: {
|
replyMode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: Object.values(CCWEB_REPLY_MODES),
|
enum: Object.values(CCWEB_REPLY_MODES),
|
||||||
description: '回传模式。one_way 表示单向投递且目标输出只留在目标对话;return_and_continue 表示目标完成后自动回传结果并触发来源继续运行。',
|
description: '回传模式。one_way 表示单向投递且目标输出只留在目标对话;return_and_continue 表示目标完成后由系统异步回传结果并触发来源继续运行。两种模式都立即返回,不要 sleep 或轮询等待。',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['targetConversationId', 'content', 'replyMode'],
|
required: ['targetConversationId', 'content', 'replyMode'],
|
||||||
@@ -307,6 +315,7 @@ const TOOLS = [
|
|||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...SCRIPT_TOOL_DEFINITIONS,
|
||||||
];
|
];
|
||||||
|
|
||||||
function imageMimeFromPath(filePath) {
|
function imageMimeFromPath(filePath) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user