fix: 对账重试 Todo 完成操作
This commit is contained in:
@@ -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` 等可能重复创建资源的命令不做盲重试。
|
||||
|
||||
---
|
||||
|
||||
## 记录管理
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user