fix: 对账重试 Todo 完成操作

This commit is contained in:
Codex
2026-07-10 07:23:31 +02:00
parent 71ec3a6264
commit 6741587134
2 changed files with 47 additions and 4 deletions
+2
View File
@@ -35,6 +35,8 @@ unidesk todo-note complete 大论文 <todo-id>
unidesk todo-note remind 大论文 <todo-id> --at 2026-07-31T18:00:00+08:00
```
`complete/reopen` 是幂等资源命令:遇到上游代理 `502/503/504` 时,CLI 会先读取实际 todo 状态,未达到目标状态才重试一次,并在第二次异常后做最终复核。`add` 等可能重复创建资源的命令不做盲重试。
---
## 记录管理
+45 -4
View File
@@ -35,6 +35,13 @@ interface FlatTodo {
type TodoNoteFetcher = (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>;
class TodoNoteRequestError extends Error {
constructor(message: string, readonly status: number | null) {
super(message);
this.name = "TodoNoteRequestError";
}
}
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
@@ -69,12 +76,22 @@ function errorMessage(body: unknown): string {
return typeof record?.error === "string" ? record.error : typeof record?.message === "string" ? record.message : "request failed";
}
function responseStatus(value: unknown): number | null {
const status = typeof value === "number" ? value : Number(value);
return Number.isInteger(status) && status > 0 ? status : null;
}
function isRetryableProxyError(error: unknown): boolean {
return error instanceof TodoNoteRequestError && error.status !== null && [502, 503, 504].includes(error.status);
}
async function request(fetcher: TodoNoteFetcher, path: string, init?: { method?: string; body?: unknown }): Promise<unknown> {
const response = asRecord(await fetcher(`/api/microservices/todo-note/proxy${path}`, init));
if (response === null) throw new Error(`todo-note ${path} returned an invalid response`);
const body = response.body;
if (response.ok !== true || asRecord(body)?.ok === false) {
throw new Error(`todo-note ${init?.method ?? "GET"} ${path} failed (${String(response.status ?? "unknown")}): ${errorMessage(body)}`);
const status = responseStatus(response.status);
throw new TodoNoteRequestError(`todo-note ${init?.method ?? "GET"} ${path} failed (${String(status ?? "unknown")}): ${errorMessage(body)}`, status);
}
return body;
}
@@ -170,6 +187,30 @@ async function applyAction(fetcher: TodoNoteFetcher, instanceId: string, action:
};
}
async function applyCompletion(
fetcher: TodoNoteFetcher,
summary: TodoInstanceSummary,
todoId: string,
desiredCompleted: boolean,
): Promise<{ instance: TodoInstance; changed: boolean }> {
const apply = () => applyAction(fetcher, summary.id, { type: "toggleTodoCompleted", todoId });
try {
return { instance: await apply(), changed: true };
} catch (firstError) {
if (!isRetryableProxyError(firstError)) throw firstError;
const observed = await getInstance(fetcher, summary);
if (resolveTodo(observed, todoId).item.completed === desiredCompleted) return { instance: observed, changed: true };
try {
return { instance: await apply(), changed: true };
} catch (secondError) {
if (!isRetryableProxyError(secondError)) throw secondError;
const final = await getInstance(fetcher, summary);
if (resolveTodo(final, todoId).item.completed === desiredCompleted) return { instance: final, changed: true };
throw secondError;
}
}
}
function short(value: string, max = 64): string {
const compact = value.replace(/\s+/gu, " ").trim();
return compact.length <= max ? compact : `${compact.slice(0, Math.max(1, max - 3))}...`;
@@ -376,9 +417,9 @@ async function runTodoNoteCommandInner(args: string[], fetcher: TodoNoteFetcher)
? { ok: true, changed: false, instance, todo: selected.item }
: mutationResult(`todo-note ${action}`, action, instance, selected, false);
}
const updated = await applyAction(fetcher, instance.id, { type: "toggleTodoCompleted", todoId: selected.item.id });
const changed = resolveTodo(updated, selected.item.id);
return json ? { ok: true, changed: true, instance: updated, todo: changed.item } : mutationResult(`todo-note ${action}`, action, updated, changed);
const result = await applyCompletion(fetcher, summary, selected.item.id, desiredCompleted);
const changed = resolveTodo(result.instance, selected.item.id);
return json ? { ok: true, changed: result.changed, instance: result.instance, todo: changed.item } : mutationResult(`todo-note ${action}`, action, result.instance, changed, result.changed);
}
if (action === "remind") {