From e15786d99030efba35d2baf682638b526678df67 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 17 Jul 2026 09:09:11 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Sub2API=20=E6=96=B0?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E6=97=B6=E9=97=B4=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unidesk-sub2api/references/codex-pool.md | 1 + .../R12.1_Task_Report.md | 25 ++++++++++ docs/MDTODO/sub2api-upstream-reliability.md | 4 ++ .../runtime-remote-script-tail.ts | 46 ++++++++++++++++++- .../runtime-render.ts | 12 ++++- 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 docs/MDTODO/details/sub2api-upstream-reliability/R12.1_Task_Report.md diff --git a/.agents/skills/unidesk-sub2api/references/codex-pool.md b/.agents/skills/unidesk-sub2api/references/codex-pool.md index 3ffeba15..6c9bea0a 100644 --- a/.agents/skills/unidesk-sub2api/references/codex-pool.md +++ b/.agents/skills/unidesk-sub2api/references/codex-pool.md @@ -7,6 +7,7 @@ - `runtime list|get` 查询 pool group 内全部账号或指定账号的实际状态;`get` 同时披露脱敏 `modelMapping` 和完整临时不可调度规则; - 新增手工账号的模板接入使用最短路径: - 先执行 `runtime list --recent `,按原生 `created_at` 倒序查看新近账号、完整名称、ID、调度状态和现有模板; + - 原生 ISO-8601 时间的小数秒先归一到 Python 可解析精度,避免非 3/6 位小数秒漏掉新账号;对首次详情投影中 `created_at` 缺失、但 `updated_at` 落窗的账号,命令自动精确重读一次;只有重读后的原生 `created_at` 落窗才纳入,`updated_at` 不作为新账号替代依据;默认摘要披露 rechecked、recovered 和 updatedUnknownExcluded 计数,并有界列出刚更新但创建时间仍无法解析的账号; - `created_at` 缺失的账号只计入 `unknownCreatedAt`,不猜测为新账号; - 选定后仍以精确 `--account ` 依次执行 dry-run 和 confirm,禁止把“最新账号”隐式变成写入 selector; - 当前正式只读入口为 `runtime get --account` 和 `runtime errors --account --full`;前者读取账号配置与调度状态,后者读取原生 Ops 已观测模型证据,不把历史请求模型误报为实时上游能力; diff --git a/docs/MDTODO/details/sub2api-upstream-reliability/R12.1_Task_Report.md b/docs/MDTODO/details/sub2api-upstream-reliability/R12.1_Task_Report.md new file mode 100644 index 00000000..533de3ca --- /dev/null +++ b/docs/MDTODO/details/sub2api-upstream-reliability/R12.1_Task_Report.md @@ -0,0 +1,25 @@ +# R12.1 任务报告 + +## 根因 + +- PK01 Sub2API 账号 #41 的原生 `created_at` 为 `2026-07-17T14:32:36.41159+08:00`,小数秒是 5 位。 +- runtime 远端 Python 使用 `datetime.fromisoformat` 直接解析;目标 Python 对非 3/6 位小数秒返回失败,导致 #41 被计为创建时间未知并从 `--recent 2h` 严格结果中排除。 +- 精确 `runtime get` 与全表仍能显示账号,因此原流程被迫增加全表和逐账号查询。 + +## 修复 + +- `parse_native_time` 在解析前把 ISO-8601 小数秒补齐或截断到 6 位,保持时区和原生 `created_at` 语义。 +- 首次详情中 `created_at` 仍缺失且 `updated_at` 落窗时,精确重读一次;只有重读后的原生创建时间落窗才纳入,禁止用更新时间替代创建时间。 +- recent 默认摘要新增 rechecked、recovered、updatedUnknownExcluded,并有界列出刚更新但创建时间仍不可解析的账号。 +- Sub2API skill 同步最短路径与判定边界。 + +## 验证 + +- `bun scripts/cli.ts check --syntax-only`:11 项通过,0 项失败。 +- `git diff --check`:通过。 +- 从任务 worktree 执行只读 `runtime list --target PK01 --recent 2h`:一次返回 #41 与 #40,按创建时间倒序;`matched=2`、`unknownCreatedAt=0`、`rechecked=0`、`updatedUnknownExcluded=0`。 +- 同一张表直接显示 #41 priority 8、#40 priority 2,两个账号均匹配 9 条 `codex-upstream-failover` 规则,不再需要全表与逐账号下钻。 + +## 边界 + +- 未修改 Sub2API 源码、版本、runtime、账号、YAML 或生产运行面;真实 smoke 仅执行原生只读查询。 diff --git a/docs/MDTODO/sub2api-upstream-reliability.md b/docs/MDTODO/sub2api-upstream-reliability.md index 5d743b7a..ff8f097b 100644 --- a/docs/MDTODO/sub2api-upstream-reliability.md +++ b/docs/MDTODO/sub2api-upstream-reliability.md @@ -327,6 +327,10 @@ 完成 [UniDesk #2361](https://github.com/pikasTech/unidesk/issues/2361):增强 Sub2API `runtime list --recent `,使用原生账号创建时间过滤并倒序展示新近账号,写入仍要求精准 selector;同时让 Sub2API rollout worker 发出阶段进度,使默认 `job status` 直接展示 prepull、apply、verify、终态和耗时,减少人工全表判断与 `--full` 下钻;只做本地 fixture/smoke,不修改生产运行面,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R12_Task_Report.md)。 +### R12.1 [completed] + +修复 [UniDesk #2361](https://github.com/pikasTech/unidesk/issues/2361) 的 `runtime list --recent` 新账号漏报:保持原生 `created_at` 严格选新和精准 selector 写入边界,归一 ISO-8601 小数秒解析,并对刚更新但创建时间未知的账号精确重读与有界披露,使新账号发现无需全表和逐账号猜测;只修改 UniDesk CLI、skill 与 MDTODO,使用只读 smoke 验证,不修改 Sub2API 源码、版本、runtime 或生产运行面,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R12.1_Task_Report.md)。 + ## R13 [completed] 调优 PK01 Sub2API 账号池:综合最近 8 小时所有分组的客户可见错误、上游归因、账号评分、普通与 compact TTFT、请求、Token、API USD、人民币边际成本、failover、临时不可调度规则命中和当前全局 priority,区分非上游问题后生成保守方案;仅对证据充分账号执行精准 runtime dry-run、confirm 和回读,新窗口短回归,不修改 Sub2API 源码、版本、凭据、分组、代理、容量、Secret 或外部哨兵,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R13_Task_Report.md)。 diff --git a/scripts/src/platform-infra-sub2api-codex/runtime-remote-script-tail.ts b/scripts/src/platform-infra-sub2api-codex/runtime-remote-script-tail.ts index 84a774ba..1447f495 100644 --- a/scripts/src/platform-infra-sub2api-codex/runtime-remote-script-tail.ts +++ b/scripts/src/platform-infra-sub2api-codex/runtime-remote-script-tail.ts @@ -129,7 +129,13 @@ def parse_native_time(value): if not isinstance(value, str) or not value: return None try: - parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + normalized = value.replace("Z", "+00:00") + normalized = re.sub( + r"\\.(\\d+)(?=(?:[+-]\\d{2}:\\d{2})?$)", + lambda match: "." + (match.group(1) + "000000")[:6], + normalized, + ) + parsed = datetime.fromisoformat(normalized) return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) except ValueError: return None @@ -1262,8 +1268,42 @@ def runtime_result(): recent_filter = None if recent: cutoff = parse_native_time(since_start_time(recent)) + rechecked_count = 0 + recovered_count = 0 + refreshed_details = [] + for item in details: + created_at = parse_native_time(item.get("created_at")) + updated_at = parse_native_time(item.get("updated_at")) + should_recheck = ( + cutoff is not None + and created_at is None + and updated_at is not None + and updated_at >= cutoff + and item.get("id") is not None + ) + if should_recheck: + rechecked_count += 1 + refreshed = account_detail(token, item["id"]) + refreshed_created_at = parse_native_time(refreshed.get("created_at")) + if refreshed_created_at is not None and refreshed_created_at >= cutoff: + recovered_count += 1 + item = refreshed + refreshed_details.append(item) + details = refreshed_details known = [(item, parse_native_time(item.get("created_at"))) for item in details] unknown_count = sum(1 for _, created_at in known if created_at is None) + recently_updated_excluded = [ + { + "accountId": item.get("id"), + "accountName": item.get("name"), + "createdAt": item.get("created_at"), + "updatedAt": item.get("updated_at"), + "reason": "created-at-unknown", + } + for item, created_at in known + if created_at is None + and (parse_native_time(item.get("updated_at")) or datetime.min.replace(tzinfo=timezone.utc)) >= (cutoff or datetime.max.replace(tzinfo=timezone.utc)) + ] details = [item for item, created_at in known if created_at is not None and cutoff is not None and created_at >= cutoff] details = sorted(details, key=lambda value: parse_native_time(value.get("created_at")) or datetime.min.replace(tzinfo=timezone.utc), reverse=True) recent_filter = { @@ -1271,6 +1311,10 @@ def runtime_result(): "cutoff": cutoff.isoformat().replace("+00:00", "Z") if cutoff is not None else None, "matchedCount": len(details), "unknownCreatedAtCount": unknown_count, + "recheckedAccountCount": rechecked_count, + "recoveredAccountCount": recovered_count, + "recentlyUpdatedUnknownCreatedAtCount": len(recently_updated_excluded), + "recentlyUpdatedUnknownCreatedAtAccounts": recently_updated_excluded[:8], } else: details = sorted(details, key=lambda value: value.get("name") or "") diff --git a/scripts/src/platform-infra-sub2api-codex/runtime-render.ts b/scripts/src/platform-infra-sub2api-codex/runtime-render.ts index 36b1905f..d852e7c4 100644 --- a/scripts/src/platform-infra-sub2api-codex/runtime-render.ts +++ b/scripts/src/platform-infra-sub2api-codex/runtime-render.ts @@ -544,7 +544,17 @@ function renderRuntimeAccountList(lines: string[], runtime: Record 0) { + lines.push("RECENTLY UPDATED EXCLUSIONS", renderTable([ + ["NAME", "ID", "CREATED", "UPDATED", "REASON"], + ...exclusions.map((account) => [ + runtimeShort(account.accountName, 36), runtimeText(account.accountId), runtimeTimestamp(account.createdAt), + runtimeTimestamp(account.updatedAt), runtimeText(account.reason), + ]), + ])); + } } lines.push(renderTable([ ["NAME", "ID", "CREATED", "MANAGEMENT", "STATUS", "SCHED", "CONCURRENCY", "LOAD", "PRIORITY", "TEMP_RULES", "TEMPLATE"],