diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index ad55e58..0dc2911 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -2,6 +2,19 @@ ## [Unreleased] +### ???? + +- **[proxy/usage]**: ???? usage ?????? ? by lsy + - ??: ?????????? + - ??: apps/worker/src/routes/proxy.ts:70-560, .helloagents/modules/proxy.md:20-40, .helloagents/modules/usage.md:10-30 + + +### ?? + +- **[proxy/usage]**: ?? usage ???????????? ? by lsy + - ??: [202603181533_usage-event-call-fix](archive/2026-03/202603181533_usage-event-call-fix/) + + ### 新增 - **[deploy-workflow]**: 新增本地一键部署脚本(init/update) — by lsy @@ -227,6 +240,9 @@ ### 快速修改 +- **[deploy-workflow]**: 本地部署脚本移除 .env 自动生成与加载 — by lsy + - 类型: 快速修改(无方案包) + - 文件: scripts/deploy.mjs:1-292 - **[admin-ui/settings]**: 缓存 TTL 与版本成对展示为卡片 — by lsy - 类型: 快速修改(无方案包) - 文件: apps/ui/src/features/SettingsView.tsx:417-632 diff --git a/.helloagents/modules/proxy.md b/.helloagents/modules/proxy.md index e7b5a20..7694881 100644 --- a/.helloagents/modules/proxy.md +++ b/.helloagents/modules/proxy.md @@ -25,6 +25,7 @@ - 对 `/v1/responses` 且上游返回 400/404 时回退为 `/responses` 重试一次 - 可配置失败重试轮询(响应 5xx/429 时触发) - 记录流式请求标记、首 token 延迟与推理强度到 usage_logs +- usage 事件调度仅传 event 对象,失败请求也必须记录 error usage - 使用日志与模型能力写入可通过 `USAGE_QUEUE` 异步化(队列不可用时自动回退同步写入) - 队列写入采用“两态简化逻辑”:未超限时按比例直写/队列,超限后全部直写 - 队列日限额由 `UsageLimiter` DO 维护,限制 `USAGE_QUEUE` 的日内发送量 diff --git a/apps/worker/src/routes/proxy.ts b/apps/worker/src/routes/proxy.ts index 66c2df6..d3054e3 100644 --- a/apps/worker/src/routes/proxy.ts +++ b/apps/worker/src/routes/proxy.ts @@ -531,7 +531,7 @@ proxy.all("/*", tokenAuth, async (c) => { }) => { const latencyMs = Date.now() - requestStart; const errorMessage = options.message ?? options.code; - scheduleUsageEvent(c, { + scheduleUsageEvent({ type: "usage", payload: { tokenId: tokenRecord.id, @@ -938,7 +938,7 @@ proxy.all("/*", tokenAuth, async (c) => { selectedUpstreamModel = upstreamModel; lastErrorDetails = null; if (recordModel) { - scheduleUsageEvent(c, { + scheduleUsageEvent({ type: "capability_upsert", payload: { channelId: channel.id, @@ -960,7 +960,7 @@ proxy.all("/*", tokenAuth, async (c) => { errorInfo.errorCode, ); if (recordModel && cooldownSeconds > 0 && cooldownEligible) { - scheduleUsageEvent(c, { + scheduleUsageEvent({ type: "model_error", payload: { channelId: channel.id, @@ -976,7 +976,7 @@ proxy.all("/*", tokenAuth, async (c) => { cooldownSeconds > 0 && cooldownEligible ) { - scheduleUsageEvent(c, { + scheduleUsageEvent({ type: "model_error", payload: { channelId: channel.id, @@ -1012,7 +1012,7 @@ proxy.all("/*", tokenAuth, async (c) => { isTimeout ? "timeout" : "exception", ); if (recordModel && cooldownSeconds > 0 && cooldownEligible) { - scheduleUsageEvent(c, { + scheduleUsageEvent({ type: "model_error", payload: { channelId: channel.id, @@ -1028,7 +1028,7 @@ proxy.all("/*", tokenAuth, async (c) => { cooldownSeconds > 0 && cooldownEligible ) { - scheduleUsageEvent(c, { + scheduleUsageEvent({ type: "model_error", payload: { channelId: channel.id, @@ -1060,7 +1060,7 @@ proxy.all("/*", tokenAuth, async (c) => { latency_ms: latencyMs, last_channel_id: lastChannel?.id ?? null, }); - scheduleUsageEvent(c, { + scheduleUsageEvent({ type: "usage", payload: { tokenId: tokenRecord.id, @@ -1113,7 +1113,7 @@ proxy.all("/*", tokenAuth, async (c) => { ? usageMissingReason ?? "usage_missing" : null : errorDetails?.errorMessage ?? null; - scheduleUsageEvent(c, { + scheduleUsageEvent({ type: "usage", payload: { tokenId: tokenRecord.id, diff --git a/scripts/deploy.mjs b/scripts/deploy.mjs index 6d1eb1a..9d2a390 100644 --- a/scripts/deploy.mjs +++ b/scripts/deploy.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node import { spawn } from "node:child_process"; -import { access, readFile, writeFile } from "node:fs/promises"; +import { access } from "node:fs/promises"; import { existsSync } from "node:fs"; import path from "node:path"; import { createInterface } from "node:readline/promises"; @@ -8,8 +8,6 @@ import { fileURLToPath } from "node:url"; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const DEFAULT_CONFIG = "apps/worker/wrangler.toml"; -const DEFAULT_ENV_FILE = path.join(ROOT, ".env"); -const DEFAULT_ENV_EXAMPLE = path.join(ROOT, ".env.example"); const BUN_CMD = (() => { if (process.env.BUN_BIN && existsSync(process.env.BUN_BIN)) { return process.env.BUN_BIN; @@ -159,56 +157,6 @@ const runCapture = (command, args, options = {}) => const runBun = (args, options) => run(BUN_CMD, args, options); const runBunx = (args, options) => run(BUN_CMD, ["x", ...args], options); -const parseEnvLine = (line) => { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) { - return null; - } - const idx = trimmed.indexOf("="); - if (idx === -1) { - return null; - } - const key = trimmed.slice(0, idx).trim(); - let value = trimmed.slice(idx + 1).trim(); - if ( - (value.startsWith("\"") && value.endsWith("\"")) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - return { key, value }; -}; - -const loadEnvFile = async (filePath) => { - if (!existsSync(filePath)) { - return []; - } - const raw = await readFile(filePath, "utf8"); - const lines = raw.split(/\r?\n/); - for (const line of lines) { - const parsed = parseEnvLine(line); - if (!parsed) { - continue; - } - if (process.env[parsed.key] === undefined) { - process.env[parsed.key] = parsed.value; - } - } - return lines; -}; - -const ensureEnvFile = async (filePath, examplePath) => { - if (existsSync(filePath)) { - return; - } - if (existsSync(examplePath)) { - const template = await readFile(examplePath, "utf8"); - await writeFile(filePath, template, "utf8"); - return; - } - await writeFile(filePath, "# 本地部署环境变量(自动生成)\n", "utf8"); -}; - const promptChoice = async (rl, label, choices, defaultValue) => { const choicesText = choices.join("/"); const defaultText = defaultValue ? ` [${defaultValue}]` : ""; @@ -340,8 +288,6 @@ const main = async () => { const configPath = path.resolve(ROOT, options.config); await access(configPath); - await ensureEnvFile(DEFAULT_ENV_FILE, DEFAULT_ENV_EXAMPLE); - await loadEnvFile(DEFAULT_ENV_FILE); if (options.action === "init") { options.target = "both";