# Ticket Manager

`ticket-manager` 是当前仓库里最完整的服务。它负责：

- 按需同步 Jira issue 并维护 case snapshot
- 通过 Dispatch Policy 执行和审计 triage
- 管理基于 Policy 的 Dispatch 配置
- 将 Lark 消息交给 Agent Gateway 处理

## Main Routes

- `POST /public/v1/ticketmanager/channels/lark/events`
- `POST /api/v1/ticketmanager/tickets/{jiraIssueKey}`
- `PUT /api/v1/ticketmanager/tickets/{jiraIssueKey}/jira_issue`
- `POST /api/v1/ticketmanager/dispatch/triggers`
- `GET /api/v1/ticketmanager/dispatch/projects/{projectKey}/components`
- `GET /api/v1/ticketmanager/engineers`
- `GET /api/v1/ticketmanager/tickets`
- `GET /api/v1/ticketmanager/tickets/{jiraIssueKey}`
- `GET /api/v1/ticketmanager/tickets/{jiraIssueKey}/timeline`
- `GET /api/v1/ticketmanager/workspace/workspaces/{id}/curated-timeline`
- `GET /api/v1/ticketmanager/workspace/workspaces/{id}/timeline-evidence`

## Run

```bash
cd services/ticket-manager
go run ./cmd/ticket-manager --config config.local.yaml
```

`--config` 是可选的；不传只走默认值 + ASM secret 覆盖（不够跑通真实流程，仅适合最小烟测）。默认监听：

```text
:8080
```

## Configuration

服务的配置入口是 [`config.RootConfig`](config/root_config.go)，全部走 YAML，没有需要单独读的环境变量。配置按下面这条链合并加载（后面的覆盖前面的）：

1. **Defaults** — 来自 Go 结构体上的 `default:"…"` tag。
2. **YAML file** — `--config <path>` 指向的文件。路径为空就跳过。
3. **AWS Secrets Manager** — 当上一步里 `secret.rootConfigSecretName` 非空时，从该 Secret 拉一份同结构 YAML 覆盖前面的值；用来落地凭据（DB 密码、Lark/Jira secret 等）。

Agent 运行环境由 Agent Gateway 管理。Ticket Manager 不再启动 OpenCode 子进程；旧的 `clients.opencode`、`clients.aws.bedrock`、`clients.alicloud.bailian` 配置已移除。模型认证和 Agent 工具凭据配置在执行 Agent 的环境中，不通过任务 prompt 或 Gateway 请求传递。

仓库里有两份样例供参考：

| 文件 | 用途 |
|---|---|
| [`config.example.yaml`](config.example.yaml) | 完整字段说明 + 占位值，作为模板长期 checked in。 |
| [`config.local.yaml`](config.local.yaml) | 本地开发实际使用的配置，已 gitignore；可放真实凭据，按需调整 host / agentGateway / lark 等。 |

典型本地启动：

```bash
cp config.example.yaml config.local.yaml   # 第一次
# 编辑 config.local.yaml 填入本机 MySQL、Lark dummy/真实凭据等
go run ./cmd/ticket-manager --config config.local.yaml
```

部署到 k8s 时，dev 配置走 [`infra/kustomize/envs/dev/ticket-manager/config.yaml`](../../infra/kustomize/envs/dev/ticket-manager/config.yaml)，由 kustomize 的 `configMapGenerator` 生成内容哈希后挂到 `/etc/ticket-manager/config.yaml`，Pod 启动参数为 `--config /etc/ticket-manager/config.yaml`。

## Common Commands

运行测试：

```bash
cd services/ticket-manager
make test
```

查看 e2e wiring：

```bash
cd services/ticket-manager
make e2e-dry-run
```

运行默认 image-backed smoke test（默认只跑 `workspace-timeline` case）：

```bash
cd services/ticket-manager
make e2e
```

运行 local `go run` 模式：

```bash
cd services/ticket-manager
make e2e-local
```

显式运行 workspace timeline case：

```bash
cd services/ticket-manager
make e2e-workspace-timeline-local
```

本地服务通过 `clients.agentGateway.address` 连接 Gateway。`services.agent.workingDirectory` 必须是 Agent 所在环境可访问的目录；远程 Agent 的目录不一定存在于 Ticket Manager 容器里。

指定 case / config：

```bash
cd services/ticket-manager
make e2e-local E2E_CASES="workspace-timeline" E2E_CONFIG_FILE=/path/to/config.local.yaml
```

如果 Docker e2e 访问外部 provider 时遇到 `SSL: CERTIFICATE_VERIFY_FAILED`，通常是容器内缺少本机或公司代理根证书。把 PEM 格式 CA bundle 传给 harness：

```bash
cd services/ticket-manager
E2E_CA_CERT_FILE=/path/to/ca-bundle.pem make e2e-workspace-timeline
```

Docker mode 会把该文件复制进本次 artifact 目录，并给容器里的 ticket-manager 设置 `SSL_CERT_FILE`、`REQUESTS_CA_BUNDLE`、`CURL_CA_BUNDLE`、`AWS_CA_BUNDLE` 和 `NODE_EXTRA_CA_CERTS`。

成功后清理 e2e 产物：

```bash
cd services/ticket-manager
make e2e-cleanup
```

## Runtime Notes

- 持久化是 MySQL（连接参数走 `database` 配置块）
- 三个应用入口复用 Agent Service：Dispatch 和 Timeline 调用 `RunAndWait`，Task advisor 调用 `Submit`。
- `services.agent.agentID` 统一选择所有场景、直接 Application 请求及 Lark 对话使用的 Agent，默认留空；新会话由 Gateway 选择默认 Agent，已有会话继续使用绑定的 Agent。所选 Agent 需要具备所有启用场景所需的技能（包括 Dispatch 的 `triage`）及工作目录访问能力。
- 场景通过 `services.dispatch.triage.agentModel`、`services.workspace.timeline.agentModel` 和 `services.workspace.taskAdvisor.addTask.agentModel` 指定模型，均默认留空以使用 Agent 默认模型。旧场景 `agentID` 配置已移除；原 Agent ID 不能直接作为模型 ID。steer 时忽略指定模型。
- Dispatch 的结果校验、一次同会话修复和 120 秒总预算保留在 triage runner 中。
- Timeline 的 `agentTimeout` 默认 `2m`，涵盖提交、排队和执行等待；`clients.agentGateway.timeout` 默认 `5s`，只控制提交 RPC。Gateway 的执行超时应小于调用总预算并留出排队时间。Agent ID 不隔离 Gateway 的全局 `maxConcurrency`。
- 公共封装不自动重试 Agent 执行。Task advisor 按 workspace + 触发消息绑定稳定会话，重复提交返回同一任务；提交成功不等于分析完成，也不保证一定创建建议。

### Agent task logs

提交、等待、终态日志均包含 `scene`、`business_key`、`run_id`、`agent_id`、`step`、`session_id`、`task_id` 和 `phase`。场景为 `dispatch.triage`、`workspace.timeline`、`workspace.task_advisor`。业务校验错误另记 `phase=validate`。

`agent_id` 记录 Agent Service 配置的请求 Agent，空值表示由 Gateway 选择；实际绑定可通过 `session_id` 关联 Gateway 会话。提交和等待日志还记录场景请求的 `agent_model`，它不表示底层确认的运行模型。回调从原 Session key 恢复 Agent ID，不从当前配置推断；TaskEvent 不携带模型，因此回调不推断模型。调用来源始终由 `scene` 标识。

版本化 session key 保存场景、业务标识、Agent ID 和 run ID，模型不参与 Session key 或等待器关联。回调不依赖原 HTTP context。即使服务重启，仍可关联旧回调；每个 timeline 批次使用新的 run ID，旧结果不能推进新批次。

### Single-replica recovery

本版使用单副本内存等待者，不持久化等待中的 Agent 结果。普通 timeline 重启后依靠现有定时扫描和数据库证据继续处理，AI 失败时仍回退规则草稿。飞书 timeline 保留窗口游标和心跳：遗留 `processing` 窗口在最后更新时间超过 15 分钟后可重新领取，`failed` 窗口保留现有 3 次尝试限制。扫描仍受工作区状态及时间范围限制。

调用超时/取消只停止等待，不能取消远端 Agent；迟到结果被记录并忽略。Gateway 当前不保存可查询的最终答案，因此重复的 `RunAndWait` 请求明确报错，不等待已丢失的终态。Task advisor 的 `Submit` 不创建本地等待者，重启后收到的终态仍可记录；没有增加自动重放会产生写操作的 advisor 任务。

### Agent runtime preparation

Agent Codex 镜像提供 `ticket-manager-cli` 和启用 `authsidecar` 的 `lark-cli`；`triage`、`workspace` 使用 `ticket-manager` 项目 skills，Lark 使用全局 `lark-cli` skill 组。`services.agent.workingDirectory` 应指向项目目录，默认挂载路径为 `/data/projects/ticket-manager`。Task advisor 按 workspace skill 使用 `ticket-manager-cli workspace advice create` 创建 advice。本地 `codex_cli` provider 需准备同样的工具、skills 和代理配置。

Task advisor 所在 Agent 环境（包括使用默认 Agent 时）需要：

- `DS_PROXY_URL`：workspace skill 使用固定的 `${DS_PROXY_URL}/devops/prod/ticket-manager` 路由作为 CLI 的 `--base-url`。Sidecar 负责认证和上游地址配置。
- `LARKSUITE_CLI_AUTH_PROXY`、`LARKSUITE_CLI_PROXY_KEY`、`LARKSUITE_CLI_APP_ID` 和 `LARKSUITE_CLI_BRAND`：Lark Auth Proxy 运行配置；身份设置保持 `bot`。App Secret 和真实 Tenant Access Token 仅由 Sidecar 管理。
- 已配置的模型认证、可写工作目录，以及 Sidecar 放行的飞书聊天读取和 `POST /api/v1/ticketmanager/workspace/advices` 操作。配置或操作缺失时应明确失败。

先准备 Agent 镜像、Gateway Agent ID/回调配置、Sidecar 路由和运行环境，再切换 Ticket Manager。项目种子只在项目目录不存在时初始化，已有项目和全局 skills 不会自动更新；需要同步已审阅的代理版 skills 并迁移旧的项目级 Lark skills，参见 [Agent Codex seeds](../../infra/images/agent-codex/project-seeds/README.md)。

## Lark

真实 Lark callback 路由：

```text
POST /public/v1/ticketmanager/channels/lark/events
```

最小配置（YAML，参见 [`config.example.yaml`](config.example.yaml)）：

- `clients.lark.appCode`
- `clients.lark.appSecret`
- `clients.lark.encryptKey`
- `clients.lark.verificationToken`

当前行为：

- 仅支持文本消息
- 私聊直接交给 Agent Gateway
- 群聊是否处理由 Agent Service 配置及 `@bot` 规则决定
- Agent Gateway 负责 session、task 和消息回复

## E2E Prerequisites

- `go`
- `curl`
- `jq`
- `yq` when using `--config-file` / `E2E_CONFIG_FILE`
- `mysql` client
- 一台 e2e 用的 MySQL host（参数通过 `MYSQL_HOST` / `MYSQL_PORT` / `MYSQL_USER` / `MYSQL_PASSWORD` 传入；user 需要有 `CREATE`/`DROP DATABASE` 权限）
- `docker` for default image-backed mode
- 默认 `E2E_FAKE_AGENT_GATEWAY=1`：启动本地确定性 gRPC Gateway，并通过 HTTP 回调返回 timeline 结果。测试断言 Gateway 专属标题，规则回退不能冒充成功。
- 使用真实 Gateway 时设置 `E2E_FAKE_AGENT_GATEWAY=0`、`AGENT_GATEWAY_ADDRESS` 和 `AGENT_INSTANCE_ID`；`AGENT_ID` 控制统一 Agent，默认留空；`TIMELINE_AGENT_MODEL` 控制 timeline 模型覆盖值，默认留空。提前配置对应 instance 的 HTTP 回调地址。Docker 模式中 Gateway 地址须从容器可达。
- 无数据库的 gRPC + HTTP 回调测试：`go test ./internal/services/agent -run TestTaskRunnerGatewayGRPCAndHTTPCallback`。
- Optional `E2E_CA_CERT_FILE=/path/to/ca-bundle.pem` when Docker mode needs a custom root CA for Jira or outbound provider TLS

每次 e2e 运行会创建一个一次性 database `ticket_manager_e2e_<rand>`（可用 `E2E_DB_NAME` 覆盖），跑完无论成功失败都会 `DROP DATABASE` 清理。

## E2E Cases

统一入口是仓库根目录下的 [`scripts/test/ticket-manager-e2e.sh`](../../scripts/test/ticket-manager-e2e.sh)。它会启动一次 ticket-manager，然后按顺序执行传入的业务 case，避免多个 case 反复 build / start。

可用 case：

| Case | Coverage |
|---|---|
| `workspace-timeline` | Workspace timeline evidence、AI draft、人工确认/编辑/隐藏和持久化 |

从仓库根目录直接运行完整 local e2e：

```bash
bash scripts/test/ticket-manager-e2e.sh \
  --mode local \
  --config-file services/ticket-manager/config.local.yaml \
  workspace-timeline
```

## E2E Artifacts

默认输出目录：

```text
.tmp/ticket-manager-e2e/<run-id>/
```

常用内容：

- `http/`
- `agent-gateway.log`
- `ticket-manager.log`

## Deployment

部署资产：

```text
infra/images/ticket-manager/Dockerfile
infra/kustomize/base/ticket-manager/
infra/kustomize/envs/dev/ticket-manager/
.github/workflows/ci-ticket-manager.yml
.github/workflows/deploy-ticket-manager-dev.yml
```

当前只覆盖 `dev` 环境。

渲染 manifests：

```bash
kubectl kustomize infra/kustomize/base/ticket-manager
kubectl kustomize infra/kustomize/envs/dev/ticket-manager
```
