codex-copilot-dx 0.9.15 → 0.9.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,8 @@ The dashboard does not poll in the background; refresh when needed.
62
62
  break continuations. Generated images and Codex transcripts are not deleted.
63
63
  - Optional image setup/maintenance never blocks the primary service. Disabled
64
64
  users get no CCDX image skill or MCP installation.
65
+ - Image API redirects must stay on the same HTTPS origin. Returned images must
66
+ use public HTTPS URLs; well-known NAT64 mappings follow IPv4 address rules.
65
67
  - Keep the adapter on loopback. LAN mode is only for trusted networks; do not
66
68
  expose it to the public internet. The dashboard is not an authentication wall
67
69
  for the API. Diagnostic errors may contain sensitive upstream text.
package/bin/cli.mjs CHANGED
@@ -75,11 +75,16 @@ if (CLI.command === "image") {
75
75
  }
76
76
  }
77
77
  if (CLI.command === "usage") {
78
- await printUsageSummary({
79
- format: CLI.outputFormat || "auto",
80
- output: process.stdout,
81
- });
82
- process.exit(0);
78
+ try {
79
+ await printUsageSummary({
80
+ format: CLI.outputFormat || "auto",
81
+ output: process.stdout,
82
+ });
83
+ process.exit(0);
84
+ } catch (e) {
85
+ console.error(status("err", e.message));
86
+ process.exit(1);
87
+ }
83
88
  }
84
89
  if (CLI.command === "cache") {
85
90
  try {
package/docs/REFERENCE.md CHANGED
@@ -107,7 +107,7 @@ This performs a fresh, read-only GitHub Copilot model-directory lookup without s
107
107
 
108
108
  Interactive terminals use aligned tables for `models`, `auth status`, and `usage`. Redirected or piped output retains the plain-text layout for scripts; use `--format table` or `--format plain` when a command offers an explicit format override.
109
109
 
110
- `ccdx usage` and the dashboard Usage table show token cache hit rate for each model and the aggregate: recorded cached input tokens divided by recorded input tokens, weighted by token count rather than averaging request percentages. This describes the upstream prompt cache; the local history and image-transform caches are separate. Incomplete historical records do not hide a computable ratio: `~` marks a rate derived from incomplete counts. A rate is `—` when input tokens are zero, usable totals are missing, or token counts are invalid. Older logs may omit explicit zero cache counts; new Responses usage records retain reported zeros. CLI tables keep existing token columns on narrow terminals and show rates below the table when the extra column does not fit.
110
+ `ccdx usage` and the dashboard Model Usage table show token cache hit rate for each model and the aggregate: recorded cached input tokens divided by recorded input tokens, weighted by token count rather than averaging request percentages. This describes the upstream prompt cache; the local history and image-transform caches are separate. Incomplete historical records do not hide a computable ratio: `~` marks a rate derived from incomplete counts. A rate is `—` when input tokens are zero, usable totals are missing, or token counts are invalid. Older logs may omit explicit zero cache counts; new Responses usage records retain reported zeros. CLI tables keep existing token columns on narrow terminals and show rates below the table when the extra column does not fit.
111
111
 
112
112
  ### Auto-review model
113
113
 
@@ -203,7 +203,13 @@ ccdx disable-image
203
203
 
204
204
  `image-status` never prints the API key and performs only a local, non-generating tool-readiness check. Disabling removes the stored provider credential, the CCDX-owned MCP block, and unmodified skill, reference, and helper files; user-modified files are preserved. Cached tool calls then fail closed. The Node helper creates a new file under the task's `output/imagegen` directory by default and reports its absolute path; it never overwrites an existing file or automatically repeats a failed generation. The image endpoint is contacted only by `enable-image` validation and by explicit `generate_image` or `edit_image` tool calls. Ordinary Responses, models, authentication, Fast, Auto-review, compaction, and image-input paths remain on their existing routes.
205
205
 
206
- Generated image downloads must use public HTTPS addresses, do not follow redirects, are DNS-checked against private and reserved addresses, and are capped at 32 MiB. DNS resolution and downloading share one deadline and respond to cancellation; a late DNS result cannot start a cancelled download. The local MCP route accepts calls only from the same device and permits at most two concurrent image operations. It requires `application/json` and rejects browser `Origin` headers; native Codex and Node clients do not send an Origin. The tool supports new-image generation at `1024x1024`, `1536x1024`, or `1024x1536`.
206
+ Generated image downloads must use public HTTPS addresses, do not follow redirects, are DNS-checked against private and reserved addresses, and are capped at 32 MiB. Well-known NAT64 `64:ff9b::/96` addresses apply the same IPv4 public/private rules to the embedded address, without rejecting legitimate public translations. DNS resolution and downloading share one deadline and respond to cancellation; a late DNS result cannot start a cancelled download. The local MCP route accepts calls only from the same device and permits at most two concurrent image operations. Host must name loopback, the actual local socket address (including equivalent IPv6/IPv4-mapped forms), or the explicitly configured bind hostname. Missing Host remains compatible with HTTP/1.0 and in-process clients. It requires `application/json` and rejects browser `Origin` headers; native Codex and Node clients do not send an Origin. The tool supports new-image generation at `1024x1024`, `1536x1024`, or `1024x1536`.
207
+
208
+ Image API generation/editing requests retain same-origin HTTPS redirects, with Fetch's existing POST/GET rules, a 20-hop limit and one shared timeout/cancellation signal. Cross-origin, HTTPS-to-HTTP, credential-bearing or invalid redirect targets are refused before another request is sent. Use the final API base URL when a service redirects to another origin. Redirect bodies are cancelled; generation failures are not automatically retried. The provider's returned image-download URLs still never follow redirects.
209
+
210
+ The Chat compatibility bridge ignores repeated complete tool names only when the name is declared and concatenation cannot be a fragment of another declared name. Overlapping fragments, delayed names and ambiguous/undeclared streams retain their existing assembly rules; argument deltas and tool-call IDs are not deduplicated. Native Responses routing and payloads are unchanged.
211
+
212
+ `ccdx usage` reports genuine log-read failures with the normal error line and exit code 1, without an uncaught exception dump. Missing logs remain a successful empty summary; malformed records are skipped with a warning rather than making usage unavailable.
207
213
 
208
214
  With `qwen-image-3.0-pro` or `qwen-image-3.0` using either supported protocol, CCDX also exposes `edit_image`. Ask Codex to modify a specific CCDX-generated image; it passes that image's returned `image_id` and the edit instruction to the same configured provider. Each edit returns a new image and ID while preserving the original; omitting `size` retains the source's requested dimensions. The Qwen request contains the source image again on every edit, as required by its [single-turn image editing API](https://help.aliyun.com/zh/model-studio/qwen-image-generation-and-editing-api-reference).
209
215
 
@@ -250,19 +256,21 @@ JSON reply to a streaming request is still not silently converted to SSE.
250
256
 
251
257
  After starting `ccdx`, open [http://127.0.0.1:2026/](http://127.0.0.1:2026/) in a browser. If `ADAPTER_PORT` is customized, use that port instead. The dashboard shares the existing adapter listener; CCDX does not open a browser automatically or start another service. The page and its assets require both a loopback connection and a loopback Host header, even when LAN binding is enabled.
252
258
 
253
- The dashboard loads local status, a live GitHub Copilot model directory, and a summary of local usage metadata once when opened. Each section has an explicit Refresh control; nothing polls in the background. The appearance button switches between the default dark terminal theme (sun icon) and a light theme (moon icon); the choice is stored only in that browser. The Requests card shows the running adapter's active raw and decoded per-request body limits, not request-body usage; Local History shows its in-memory Responses history use and active limit. Limit changes remain CLI-only (`ccdx limits` and `ccdx cache`). The live model table uses the same fresh account validation and selectable GPT filtering as `ccdx models`, never labels cached fallback data as online, and makes no inference request. A catalog listing does not guarantee inference access. The Usage table combines the current and rotated local usage logs, shows totals and up to 100 model rows, and contains no prompt, response body, or credential fields. The animation selector previews the existing themes and saves through the same settings writer as `ccdx animation`; changes take effect on the next adapter start, and `CCDX_TERMINAL_ANIMATION=off` still disables terminal motion. Dashboard data requests require a custom header that cross-site simple requests cannot send; animation writes also require a matching same-origin `Origin`. The dashboard does not initialize the optional image handler. The overview's known-model count is the adapter's current in-memory catalog, not the live table. A missing image-generation status does not prove the feature is disabled: use `ccdx image-status` to check setup. Recent upstream error prose may contain sensitive details, so review the page before sharing screenshots.
259
+ The dashboard loads local status, a live GitHub Copilot model directory, and a summary of local usage metadata once when opened. Each section has an explicit Refresh control; nothing polls in the background. The appearance button switches between the default dark terminal theme (sun icon) and a light theme (moon icon); the choice is stored only in that browser. The Requests card shows the running adapter's active raw and decoded per-request body limits, not request-body usage; Local History shows its in-memory Responses history use and active limit. Limit changes remain CLI-only (`ccdx limits` and `ccdx cache`). The live model table uses the same fresh account validation and selectable GPT filtering as `ccdx models`, never labels cached fallback data as online, and makes no inference request. A catalog listing does not guarantee inference access. The Model Usage table combines the current and rotated local usage logs, shows totals and up to 100 model rows, and contains no prompt, response body, or credential fields. The animation selector previews the existing themes and saves through the same settings writer as `ccdx animation`; changes take effect on the next adapter start, and `CCDX_TERMINAL_ANIMATION=off` still disables terminal motion. Dashboard data requests require a custom header that cross-site simple requests cannot send; animation writes also require a matching same-origin `Origin`. The dashboard does not initialize the optional image handler. The overview's known-model count is the adapter's current in-memory catalog, not the live table. A missing image-generation status does not prove the feature is disabled: use `ccdx image-status` to check setup. Recent upstream error prose may contain sensitive details, so review the page before sharing screenshots.
260
+
261
+ The dashboard labels the upstream token cache hit metric **Reuse Rate** and cached input **Reused Input**, separate from **Local History**. Chinese uses short functional names such as “对话暂存”, “重用输入” and “重用比例”. English and Chinese preserve model IDs, commands, units, HTTP status codes and raw diagnostic values. Animation display names translate in place; theme IDs and CLI parameters do not change.
254
262
 
255
263
  The transparent, single-color double-peak favicon follows the dashboard's saved light/dark choice without a separate network request.
256
264
 
257
- The saved GitHub Copilot account appears immediately to the left of the Snapshot time. It uses the same local credentials and fingerprint-bound identity metadata as `ccdx auth status`, with username (or numeric ID fallback) and a `SAVED` label; this is not an online entitlement check. Missing, invalid, unidentified, or unreadable credentials are shown explicitly. The account read runs once on page load and on the top Refresh action, independently of adapter status, with no polling or upstream authentication calls. Its loopback-only, custom-header-protected API exposes no token, fingerprint, credential path, or raw metadata. The general status endpoint retains its existing identity-free contract.
265
+ The saved GitHub Copilot account appears immediately to the left of the Status updated time. It uses the same local credentials and fingerprint-bound identity metadata as `ccdx auth status`, with username (or numeric ID fallback) and a `SAVED` label; this is not an online entitlement check. Missing, invalid, unidentified, or unreadable credentials are shown explicitly. The account read runs once on page load and on the top Refresh action, independently of adapter status, with no polling or upstream authentication calls. Its loopback-only, custom-header-protected API exposes no token, fingerprint, credential path, or raw metadata. The general status endpoint retains its existing identity-free contract.
258
266
 
259
267
  Account names containing underscores are supported, including `dingxiao_microsoft`. A numeric ID is a fallback, not a replacement for an available, safely displayable username; the existing length bounds and credential-shaped text suppression still apply.
260
268
 
261
269
  The language button immediately right of the appearance button switches between English and Simplified Chinese. English remains the default; the choice is saved only in the current browser, separately from light/dark mode. Switching translates the existing page in place: it does not fetch API data, rebuild charts, change selected filters, discard unsaved animation choices, or reset expanded failure details. Static labels, live status text, chart tooltips and accessibility labels follow the selected language. Model IDs, account names, dates, numeric values and upstream diagnostic prose stay unchanged; copied diagnostic fields retain their original names and values. The local translation asset adds no dependency, external service, polling or inference-path work.
262
270
 
263
- Usage Analytics is collapsed by default. Opening it adds daily input/output token bars (7/30/90 days) and a 365-day calendar of recorded usage calls or tokens, grouped in the browser's time zone. Cache-read tokens are already part of input and are not added again. Model and range filters use the current snapshot without additional requests; selecting a day shows its filtered model totals in the Usage table, and All history restores the original table. Analytics shares the existing current/rotated-log scan and exposes only bounded metadata (365 days, up to 100 model filters). Days before retained history are hatched, not presented as zero; the oldest retained day may be partial. Counts describe retained usage records, not messages, session time, or all HTTP requests. Refresh explicitly to update the snapshot; there is no background collection or polling.
271
+ Usage Trends is collapsed by default. Opening it adds daily input/output token bars (7/30/90 days) and a 365-day calendar of recorded usage calls or tokens, grouped in the browser's time zone. Cache-read tokens are already part of input and are not added again. Model and range filters use the current snapshot without additional requests; selecting a day shows its filtered model totals in the Model Usage table, and Show all restores the original table. Analytics shares the existing current/rotated-log scan and exposes only bounded metadata (365 days, up to 100 model filters). Days before retained history are hatched, not presented as zero; the oldest retained day may be partial. Counts describe retained usage records, not messages, session time, or all HTTP requests. Refresh explicitly to update the snapshot; there is no background collection or polling.
264
272
 
265
- Terminal Animation is collapsed at the bottom of the page, with an Animation shortcut in the header. Preview frames load only when opened; closing it, hiding the page, or requesting reduced motion stops the preview timer. Unsaved choices survive folding and reopening. Recent response failures expand to diagnostic fields (time, model, code, message, response/request IDs, and retry policy) with a Copy diagnostic control. The view combines the last 10 retained `response.failed` events with failures from the 20 most recently finished Responses/compact requests, showing at most 10 entries. Matching local request IDs attach the final outcome and timing metadata, so a recovered event can show `completed`; an uncorrelated retry attempt does not prove recovery. Transport failures and incomplete/cancelled requests can appear without a `response.failed` event. These are bounded runtime observations, not complete session histories. Diagnostic prose is redacted by the existing recorder but can still contain sensitive context: review before copying or sharing.
273
+ Terminal Animation is collapsed at the bottom of the page, with an Animation shortcut in the header. Preview frames load only when opened; closing it, hiding the page, or requesting reduced motion stops the preview timer. Unsaved choices survive folding and reopening. Recent response failures expand to diagnostic fields (time, model, code, message, response/request IDs, and retry policy) with a Copy details control. The view combines the last 10 retained `response.failed` events with failures from the 20 most recently finished Responses/compact requests, showing at most 10 entries. Matching local request IDs attach the final outcome and timing metadata, so a recovered event can show `completed`; an uncorrelated retry attempt does not prove recovery. Transport failures and incomplete/cancelled requests can appear without a `response.failed` event. These are bounded runtime observations, not complete session histories. Diagnostic prose is redacted by the existing recorder but can still contain sensitive context: review before copying or sharing.
266
274
 
267
275
  Inspect only the local Codex configuration:
268
276
 
@@ -326,7 +334,7 @@ ccdx --show-request-id
326
334
 
327
335
  ### Authentication recovery
328
336
 
329
- The dashboard's collapsed **RECENT REQUEST CONTEXT** section reuses the same
337
+ The dashboard's collapsed **RECENT REQUESTS** section reuses the same
330
338
  status snapshot. It shows at most 20 finished request observations: serialized
331
339
  upstream payload bytes, image count, reported input tokens, the advertised model
332
340
  window and terminal outcome, plus process-local compaction outcome counts. Missing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
package/src/adapter.mjs CHANGED
@@ -215,7 +215,7 @@ export function createAdapterHandler(options = {}) {
215
215
  if (pathname === "/mcp/image") {
216
216
  if (imageMcpHandler) return imageMcpHandler(req, res);
217
217
  return import("./image-mcp.mjs").then(({ createImageMcpHandler }) => {
218
- imageMcpHandler ||= createImageMcpHandler();
218
+ imageMcpHandler ||= createImageMcpHandler({ localHostname: options.imageMcpHostname });
219
219
  return imageMcpHandler(req, res);
220
220
  });
221
221
  }
@@ -460,7 +460,7 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
460
460
  const terminalActivity = ownsTerminalActivity
461
461
  ? createTerminalActivityIndicator({ theme: options.terminalAnimationTheme })
462
462
  : options.terminalActivity;
463
- const handler = createAdapterHandler({ ...options, terminalActivity });
463
+ const handler = createAdapterHandler({ ...options, terminalActivity, imageMcpHostname: host });
464
464
  const server = http.createServer(handler);
465
465
  const cleanupTerminalActivity = () => {
466
466
  if (ownsTerminalActivity) terminalActivity?.cleanup?.();
@@ -26,7 +26,7 @@
26
26
 
27
27
  <section class="overview" aria-label="Overview" data-i18n-label>
28
28
  <article class="card"><h2 data-i18n>ADAPTER</h2><strong id="version">—</strong><p id="uptime">—</p></article>
29
- <article class="card"><h2>COPILOT</h2><strong id="copilot">—</strong><p id="token">—</p></article>
29
+ <article class="card"><h2>Copilot</h2><strong id="copilot">—</strong><p id="token">—</p></article>
30
30
  <article class="card"><h2 data-i18n>KNOWN MODELS</h2><strong id="models">—</strong><p id="model-source">—</p></article>
31
31
  <article class="card"><h2 data-i18n>REQUESTS</h2><strong id="requests">—</strong><p id="request-errors">—</p><p><span data-i18n>Per-request limits (active)</span><br><span id="body-limits">—</span></p></article>
32
32
  </section>
@@ -58,7 +58,7 @@
58
58
  <section class="panel dashboard-section" aria-label="Live upstream models" data-i18n-label>
59
59
  <div class="panel-heading"><h2 data-i18n>UPSTREAM GPT MODELS</h2><div class="heading-actions"><span id="models-state" data-i18n>CHECKING LIVE</span><button id="refresh-models" type="button" data-i18n>Refresh</button></div></div>
60
60
  <p id="models-note" class="table-note" data-i18n>Querying GitHub Copilot…</p>
61
- <div class="table-wrap"><table><thead><tr><th scope="col" data-i18n>MODEL</th><th scope="col" data-i18n>VENDOR</th><th scope="col">API</th><th scope="col" data-i18n>FLAG</th></tr></thead><tbody id="live-models-body"><tr><td colspan="4" data-i18n>Loading…</td></tr></tbody></table></div>
61
+ <div class="table-wrap"><table><thead><tr><th scope="col" data-i18n>MODEL</th><th scope="col" data-i18n>VENDOR</th><th scope="col" data-i18n>API</th><th scope="col" data-i18n>FLAG</th></tr></thead><tbody id="live-models-body"><tr><td colspan="4" data-i18n>Loading…</td></tr></tbody></table></div>
62
62
  <p class="hint" data-i18n>Catalog listing does not guarantee inference access.</p>
63
63
  </section>
64
64
 
@@ -1,185 +1,218 @@
1
1
  (() => {
2
2
  const translations = {
3
- "CCDX · Local dashboard": "CCDX · 本地看板",
4
- "/ LOCAL STATUS": "/ 本地状态",
5
- "READING": "读取中",
6
- "LOCAL OK": "本机正常",
7
- "UNAVAILABLE": "不可用",
8
- "Refresh": "刷新",
9
- "Animation": "动效",
10
- "Switch to light mode": "切换为日间模式",
11
- "Switch to dark mode": "切换为夜间模式",
12
- "Overview": "概览",
13
- "RECENT REQUEST CONTEXT": "近期请求上下文",
14
- "WIRE MiB": "上游请求 MiB",
15
- "IMAGES": "图片数",
16
- "INPUT TOKENS": "输入 Token",
17
- "MODEL WINDOW": "模型窗口",
18
- "INPUT / WINDOW": "输入 / 窗口",
19
- "OUTCOME": "终态",
20
- "Unknown": "未知",
21
- "No observed request metadata.": "暂无已观察到的请求元数据。",
22
- "20 latest finished requests / compactions: {completed} completed, {incomplete} incomplete, {failed} failed": "最近 20 次已结束请求 / 压缩:{completed} 已完成、{incomplete} 未完成、{failed} 失败",
23
- "Recent upstream request metadata, not exact live session occupancy. Missing values are unknown. No prompt or image content is retained.": "近期上游请求元数据,不等同于会话实时精确占用。缺失值表示未知;不保留提示词或图片内容。",
24
- "Runtime details": "运行详情",
25
- "ADAPTER": "适配服务",
26
- "KNOWN MODELS": "已知模型",
27
- "REQUESTS": "请求",
28
- "Per-request limits (active)": "单次请求上限(当前)",
29
- "MODEL OUTCOMES": "模型终态",
30
- "Completed": "已完成",
31
- "Incomplete": "未完成",
32
- "Failed": "失败",
33
- "Cancelled": "已取消",
34
- "HTTP 2xx != model completed.": "HTTP 2xx 不代表模型已完成。",
35
- "LOCAL HISTORY": "本机历史缓存",
36
- "ACTIVE LIMIT": "当前上限",
37
- "History cache usage": "历史缓存用量",
38
- "IMAGE GENERATION": "图片生成",
39
- "OPTIONAL": "可选功能",
40
- "Live upstream models": "上游在线模型",
41
- "UPSTREAM GPT MODELS": "上游 GPT 模型",
42
- "CHECKING LIVE": "在线查询中",
43
- "Querying GitHub Copilot…": "正在查询 GitHub Copilot…",
44
- "MODEL": "模型",
45
- "VENDOR": "提供方",
46
- "FLAG": "标记",
47
- "Loading…": "加载中…",
48
- "Catalog listing does not guarantee inference access.": "模型出现在目录中,不代表当前账号可调用它。",
49
- "Usage by model": "模型用量",
50
- "USAGE": "用量",
51
- "READING LOG": "读取日志中",
52
- "All history": "全部历史",
53
- "Aggregating local usage metadata…": "正在汇总本机用量记录…",
54
- "CALLS": "调用次数",
55
- "INPUT": "输入",
56
- "CACHE READ": "缓存读取",
57
- "OUTPUT": "输出",
58
- "TOTAL": "合计",
59
- "CACHE HIT": "缓存命中率",
60
- "Cache hit = recorded cached / input tokens. ~ = incomplete history. — = unavailable or invalid counts.": "命中率 = 已记录的缓存输入 / 输入 Token。~ 表示历史不完整;— 表示数据不可用或无效。",
61
- "USAGE ANALYTICS": "用量分析",
62
- "Model": "模型",
63
- "All models": "全部模型",
64
- "Daily range": "统计范围",
65
- "7 days": "7 天",
66
- "30 days": "30 天",
67
- "90 days": "90 天",
68
- "Activity": "活跃指标",
69
- "Calls": "调用次数",
70
- "Tokens": "Token 数",
71
- "Open to read retained usage history.": "展开后读取已保留的用量历史。",
72
- "DAILY TOKENS": "每日 Token 用量",
73
- "INPUT + OUTPUT / CACHE INCLUDED IN INPUT": "输入 + 输出 / 缓存已包含在输入中",
74
- "Daily token usage": "每日 Token 用量",
75
- "Input": "输入",
76
- "Output": "输出",
77
- "REQUEST ACTIVITY · 365 DAYS": "调用热力图 · 365 天",
78
- "Daily recorded request activity": "每日已记录的调用次数",
79
- "Select a day to inspect its model totals. No history is distinct from zero recorded calls.": "点击日期查看模型用量。无留存历史不等于零次调用。",
80
- "Recent failures": "近期失败",
81
- "RECENT RESPONSE FAILURES": "近期响应失败",
82
- "Reading…": "读取中…",
83
- "Reading status…": "正在读取状态…",
84
- "Upstream messages may contain sensitive text.": "上游错误信息可能含敏感内容。",
85
- "TERMINAL ANIMATION": "终端动效",
86
- "Animation theme": "动效主题",
87
- "Save selection": "保存选择",
88
- "Applies on next ccdx start.": "下次启动 ccdx 时生效。",
89
- "LOCAL ONLY / NO BACKGROUND POLLING": "仅限本机 / 无后台轮询",
90
- "Saved GitHub Copilot account / local credentials only; not an online entitlement check.": "已保存的 GitHub Copilot 账号;仅读取本机凭据,不代表在线权限验证。",
91
- "GitHub / READING": "GitHub / 读取中",
92
- "GitHub / UNAVAILABLE": "GitHub / 不可用",
93
- "GitHub / NOT CONFIGURED": "GitHub / 未配置",
94
- "GitHub / INVALID": "GitHub / 无效",
95
- "GitHub / {account} / SAVED": "GitHub / {account} / 已保存",
96
- "account unknown": "账号未知",
97
- "pid {pid} / uptime {duration}": "PID {pid} / 运行时间 {duration}",
98
- "BOUND": "已绑定",
99
- "UNCONFIRMED": "未确认",
100
- "service token / TTL {duration}": "服务令牌 / 剩余有效期 {duration}",
101
- "service token not cached": "服务令牌未缓存",
102
- "source {source} / live: ccdx models": "来源 {source} / 在线目录:ccdx models",
103
- "built-in": "内置",
104
- "cache": "缓存",
105
- "live": "在线",
106
- "unknown": "未知",
107
- "4xx {client} / 5xx {server} / active {active}": "4xx {client} / 5xx {server} / 进行中 {active}",
108
- "raw {raw} / decoded {decoded}": "原始 {raw} / 解码后 {decoded}",
109
- "entries {entries} / trees {trees} / misses {misses} (evicted {evicted})": "记录 {entries} / 历史树 {trees} / 未命中 {misses}(已淘汰 {evicted})",
110
- "{count} succeeded": "成功 {count} 次",
111
- "NOT INITIALIZED": "未初始化",
112
- "active {active} / failed {failed} / delivery failures {delivery}": "进行中 {active} / 失败 {failed} / 交付失败 {delivery}",
113
- "Setup state unknown / ccdx image-status": "配置状态未知 / ccdx image-status",
114
- "Snapshot {time}": "快照 {time}",
115
- "Status unavailable / check ccdx, then refresh": "状态不可用 / 请检查 ccdx 后刷新",
116
- "{count} retained / requests + events": "保留 {count} 条 / 请求与事件",
117
- "No recent request failures recorded.": "未记录近期请求失败。",
118
- "Request ended without successful completion.": "请求结束,但未成功完成。",
119
- "{detail} / {retry}": "{detail} / {retry}",
120
- "retry attempted": "已尝试重试",
121
- "no retry": "未重试",
122
- "time": "时间",
123
- "model": "模型",
124
- "event": "事件",
125
- "code": "错误码",
126
- "message": "错误信息",
127
- "response_id": "响应 ID",
128
- "upstream_request_id": "上游请求 ID",
129
- "request_id": "本机请求 ID",
130
- "outcome": "最终状态",
131
- "origin": "终态或错误来源",
132
- "phase": "最后阶段",
133
- "http_status": "HTTP 状态",
134
- "upstream_attempts": "上游请求次数",
135
- "timings_ms": "时间线(毫秒)",
136
- "retry": "重试",
137
- "retry_policy": "重试策略",
138
- "retry_skipped": "跳过重试原因",
139
- "attempted (outcome not recorded here)": "已尝试(此处未记录结果)",
140
- "attempted": "已尝试",
141
- "not attempted": "未尝试",
142
- "Copy diagnostic": "复制诊断信息",
143
- "Copied": "已复制",
144
- "Copy unavailable": "无法复制",
145
- "DEFAULT": "默认",
146
- "Disabled by CCDX_TERMINAL_ANIMATION; saved choice applies after override removal and next start.": "CCDX_TERMINAL_ANIMATION 已禁用动效;移除覆盖设置后,保存的选择在下次启动时生效。",
147
- "Saved choice applies on next ccdx start.": "保存的选择在下次启动 ccdx 时生效。",
148
- "CURRENT {theme}{unsaved}": "当前 {theme}{unsaved}",
149
- " / UNSAVED": " / 未保存",
150
- "Saving…": "保存中…",
151
- "Not saved / {error}": "未保存 / {error}",
152
- "preview": "预览版",
153
- "No selectable GPT models advertised.": "上游未提供可选的 GPT 模型。",
154
- "LIVE SNAPSHOT": "在线快照",
155
- "{selectable} selectable / {advertised} advertised / {host} / {time}": "可选 {selectable} / 上游提供 {advertised} / {host} / {time}",
156
- "LIVE LOOKUP FAILED": "在线查询失败",
157
- "No live catalog available.": "在线模型目录不可用。",
158
- "No usage records.": "暂无用量记录。",
159
- "LOCAL LOG": "本机日志",
160
- "{records} records / {shown} of {models} models shown{day}": "记录 {records} 条 / 展示 {shown} 个模型,共 {models} 个{day}",
161
- " / {zone} / {model}": " / {zone} / {model}",
162
- "no retained history": "无留存历史",
163
- "{calls} recorded calls / input {input} / output {output} / cached {cached} (included in input){partial}": "记录 {calls} 次调用 / 输入 {input} / 输出 {output} / 缓存 {cached}(已包含在输入中){partial}",
164
- " / partial token counts": " / Token 数据不完整",
165
- "{date} / {description}": "{date} / {description}",
166
- "{metric} / PEAK {peak}": "{metric} / 峰值 {peak}",
167
- "INPUT + OUTPUT": "输入 + 输出",
168
- "RECORDED CALLS": "已记录调用",
169
- "{zone} / {from} — {to} / retained history starts {first}; oldest day may be partial.{excluded}{limit}": "{zone} / {from} — {to} / 留存历史始于 {first};最早一天的数据可能不完整。{excluded}{limit}",
170
- " {count} records with invalid/future timestamps excluded.": " 已排除 {count} 条时间无效或位于未来的记录。",
171
- " Model filters limited to 100; all-model totals include the remainder.": " 模型筛选最多展示 100 个模型;全部模型合计包含其余模型。",
172
- "Selected {date} / {model}: totals in the Usage table above / All history resets the table.": "已选 {date} / {model}:用量见上方表格 / 点击“全部历史”恢复总表。",
173
- "Select a day for model totals. Calls are usage records, not messages or time. Hatched cells: no retained history; empty cells: zero recorded calls.": "点击日期查看模型用量。调用次数来自用量记录,不代表消息数或时长。斜线格表示无留存历史;空白格表示零次已记录调用。",
174
- "LOG UNAVAILABLE": "日志不可用",
175
- "No usage summary available.": "用量汇总不可用。",
176
- "Analytics unavailable / refresh Usage to retry.": "分析不可用 / 请刷新用量后重试。",
177
- "Animation settings are invalid; inspect them with ccdx animation": "动效设置无效;请运行 ccdx animation 检查。",
178
- "Animation setting was not saved; check ccdx animation": "动效设置未保存;请运行 ccdx animation 检查。",
179
- "Could not read local usage summary": "无法读取本机用量汇总。",
180
- "Live model lookup timed out. Retry or run ccdx models.": "在线模型查询超时;请重试或运行 ccdx models。",
181
- "GitHub token not found. Start ccdx to sign in.": "未找到 GitHub 令牌;请启动 ccdx 登录。",
182
- "Live model lookup failed. Run ccdx models for details.": "在线模型查询失败;请运行 ccdx models 查看详情。",
3
+ "CCDX · Local dashboard": ["CCDX · Dashboard", "CCDX · 运行看板"],
4
+ "/ LOCAL STATUS": ["/ DASHBOARD", "/ 运行看板"],
5
+ "READING": ["READING", "读取中"],
6
+ "LOCAL OK": ["LOCAL OK", "本机正常"],
7
+ "UNAVAILABLE": ["UNAVAILABLE", "无法读取"],
8
+ "Refresh": ["Refresh", "刷新"],
9
+ "Animation": ["Animation", "动效"],
10
+ "Switch to light mode": ["Light mode", "白天模式"],
11
+ "Switch to dark mode": ["Dark mode", "夜间模式"],
12
+ "Overview": ["Overview", "概览"],
13
+ "RECENT REQUEST CONTEXT": ["RECENT REQUESTS", "近期请求"],
14
+ "WIRE MiB": ["DATA SIZE", "数据大小"],
15
+ "IMAGES": ["IMAGES", "图片数量"],
16
+ "INPUT TOKENS": ["INPUT USAGE", "输入用量"],
17
+ "MODEL WINDOW": ["MODEL CAPACITY", "模型容量"],
18
+ "INPUT / WINDOW": ["CAPACITY USED", "占用比例"],
19
+ "OUTCOME": ["RESULT", "结果"],
20
+ "Unknown": ["Unknown", "未知"],
21
+ "No observed request metadata.": ["No request records yet.", "暂未收到请求记录。"],
22
+ "20 latest finished requests / compactions: {completed} completed, {incomplete} incomplete, {failed} failed": ["Last 20 finished requests.\nHistory compaction: {completed} completed · {incomplete} incomplete · {failed} failed", "最近20次已结束请求。\n压缩完成:{completed}次\n压缩未完成:{incomplete}次\n压缩失败:{failed}次"],
23
+ "Recent upstream request metadata, not exact live session occupancy. Missing values are unknown. No prompt or image content is retained.": ["Size: MiB. Usage and capacity: Token.\nRequest records, not exact current thread usage.\nMissing data is unknown. No prompts or images are saved.", "大小单位:MiB。\n用量单位:Token。\n不是会话当前的精确占用。\n缺失数据表示未知。\n不保存提问或图片内容。"],
24
+ "Runtime details": ["Runtime details", "运行详情"],
25
+ "ADAPTER": ["SERVICE", "服务状态"],
26
+ "KNOWN MODELS": ["MODEL COUNT", "模型数量"],
27
+ "REQUESTS": ["REQUESTS", "请求次数"],
28
+ "Per-request limits (active)": ["Per-request limits", "单次上限"],
29
+ "MODEL OUTCOMES": ["REQUEST RESULTS", "处理结果"],
30
+ "Completed": ["Completed", "已完成"],
31
+ "Incomplete": ["Incomplete", "未完成"],
32
+ "Failed": ["Failed", "失败"],
33
+ "Cancelled": ["Cancelled", "已取消"],
34
+ "HTTP 2xx != model completed.": ["HTTP 2xx can still mean an unfinished reply.", "HTTP 2xx也可能没答完整。"],
35
+ "LOCAL HISTORY": ["LOCAL HISTORY", "对话暂存"],
36
+ "ACTIVE LIMIT": ["STORAGE LIMIT", "存储上限"],
37
+ "History cache usage": ["Local history usage", "对话暂存"],
38
+ "IMAGE GENERATION": ["IMAGE GENERATION", "图片生成"],
39
+ "OPTIONAL": ["OPT-IN", "按需开启"],
40
+ "Live upstream models": ["Online models", "在线模型"],
41
+ "UPSTREAM GPT MODELS": ["ONLINE MODELS", "在线模型"],
42
+ "CHECKING LIVE": ["CHECKING", "查询中"],
43
+ "Querying GitHub Copilot…": ["Checking Copilot models…", "正在查询Copilot模型。"],
44
+ "MODEL": ["MODEL", "模型"],
45
+ "VENDOR": ["PROVIDER", "提供方"],
46
+ "API": ["INTERFACE", "连接方式"],
47
+ "FLAG": ["STAGE", "阶段"],
48
+ "Loading…": ["Loading…", "加载中"],
49
+ "Catalog listing does not guarantee inference access.": ["A listed model may still be unavailable to your account.", "模型列出后,仍可能无法使用。"],
50
+ "Usage by model": ["Model usage", "模型用量"],
51
+ "USAGE": ["MODEL USAGE", "模型用量"],
52
+ "READING LOG": ["COUNTING", "统计中"],
53
+ "All history": ["Show all", "查看全部"],
54
+ "Aggregating local usage metadata…": ["Counting local usage…", "正在统计本机用量。"],
55
+ "CALLS": ["REQUESTS", "请求次数"],
56
+ "INPUT": ["INPUT USAGE", "输入用量"],
57
+ "CACHE READ": ["REUSED INPUT", "重用输入"],
58
+ "OUTPUT": ["OUTPUT USAGE", "输出用量"],
59
+ "TOTAL": ["TOTAL", "总用量"],
60
+ "CACHE HIT": ["REUSE RATE", "重用比例"],
61
+ "Cache hit = recorded cached / input tokens. ~ = incomplete history. — = unavailable or invalid counts.": ["Usage unit: Token. Reuse rate = reused input / input usage.\n~ means incomplete records. — means unknown or invalid data.", "用量单位:Token。\n重用输入占输入用量的比例。\n~:记录不完整。\n—:数据未知或无效。"],
62
+ "USAGE ANALYTICS": ["USAGE TRENDS", "用量趋势"],
63
+ "Model": ["Model", "模型"],
64
+ "All models": ["All models", "全部模型"],
65
+ "Daily range": ["Date range", "时间范围"],
66
+ "7 days": ["Last 7 days", "近7天"],
67
+ "30 days": ["Last 30 days", "近30天"],
68
+ "90 days": ["Last 90 days", "近90天"],
69
+ "Activity": ["Measure", "统计内容"],
70
+ "Calls": ["Requests", "请求次数"],
71
+ "Tokens": ["Usage (Token)", "处理用量"],
72
+ "Open to read retained usage history.": ["Open to view usage trends.", "展开查看用量趋势。"],
73
+ "DAILY TOKENS": ["DAILY USAGE", "每日用量"],
74
+ "INPUT + OUTPUT / CACHE INCLUDED IN INPUT": ["INPUT + OUTPUT · REUSED INPUT INCLUDED", "输入与输出合计。\n重用部分已计入输入用量。"],
75
+ "Daily token usage": ["Daily usage in Token", "每日用量"],
76
+ "Input": ["Input usage", "输入用量"],
77
+ "Output": ["Output usage", "输出用量"],
78
+ "REQUEST ACTIVITY · 365 DAYS": ["ACTIVITY · LAST 365 DAYS", "活跃日历"],
79
+ "Daily recorded request activity": ["Daily recorded activity over 365 days", "每日活跃"],
80
+ "Select a day to inspect its model totals. No history is distinct from zero recorded calls.": ["Select a day to view its usage.\nNo records is different from zero requests.", "点击日期,查看当天用量。\n没有记录,不等于零次请求。"],
81
+ "Recent failures": ["Recent issues", "异常记录"],
82
+ "RECENT RESPONSE FAILURES": ["RECENT ISSUES", "异常记录"],
83
+ "Reading…": ["Reading…", "读取中"],
84
+ "Reading status…": ["Reading status…", "读取中"],
85
+ "Upstream messages may contain sensitive text.": ["Error details may contain private information.", "错误内容可能含私人信息。"],
86
+ "TERMINAL ANIMATION": ["TERMINAL ANIMATION", "终端动效"],
87
+ "Animation theme": ["Animation style", "动效样式"],
88
+ "Save selection": ["Save animation", "保存动效"],
89
+ "Applies on next ccdx start.": ["Save, then restart ccdx to apply.", "保存后,下次启动生效。"],
90
+ "LOCAL ONLY / NO BACKGROUND POLLING": ["LOCAL ONLY · NO TIMED REFRESH", "仅在本机查看。不会定时读取。"],
91
+ "Saved GitHub Copilot account / local credentials only; not an online entitlement check.": ["Saved local sign-in details only.\nOnline access has not been checked.", "仅查看本机保存的登录信息。\n尚未核实在线使用权限。"],
92
+ "GitHub / READING": ["GitHub / READING", "GitHub / 读取中"],
93
+ "GitHub / UNAVAILABLE": ["GitHub / UNAVAILABLE", "GitHub / 无法读取"],
94
+ "GitHub / NOT CONFIGURED": ["GitHub / NOT SET UP", "GitHub / 未设置"],
95
+ "GitHub / INVALID": ["GitHub / INVALID DETAILS", "GitHub / 信息无效"],
96
+ "GitHub / {account} / SAVED": ["GitHub / {account} / SAVED", "GitHub / {account} / 已保存"],
97
+ "account unknown": ["account unknown", "账号未知"],
98
+ "pid {pid} / uptime {duration}": ["PID {pid} · running {duration}", "进程编号:{pid} · 已运行:{duration}"],
99
+ "BOUND": ["IDENTIFIED", "已识别"],
100
+ "UNCONFIRMED": ["UNCONFIRMED", "待确认"],
101
+ "service token / TTL {duration}": ["Access details expire in {duration}", "有效期:{duration}"],
102
+ "service token not cached": ["No saved access details yet.", "还没有暂存授权信息。"],
103
+ "source {source} / live: ccdx models": ["Source: {source}\nOnline list: ccdx models", "来源:{source}\n在线目录:ccdx models"],
104
+ "built-in": ["built-in list", "内置目录"],
105
+ "cache": ["existing list", "已有目录"],
106
+ "live": ["online list", "在线目录"],
107
+ "unknown": ["unknown", "未知"],
108
+ "4xx {client} / 5xx {server} / active {active}": ["4xx {client} · 5xx {server} · active {active}", "4xx {client} · 5xx {server} · 进行中 {active}"],
109
+ "raw {raw} / decoded {decoded}": ["Received: {raw}\nUnpacked: {decoded}", "接收上限:{raw}\n展开上限:{decoded}"],
110
+ "entries {entries} / trees {trees} / misses {misses} (evicted {evicted})": ["Records: {entries} · groups: {trees}\nNot found: {misses} · after removal: {evicted}", "记录:{entries}条 · 历史分组:{trees}组\n未找到:{misses}次\n清出后未找到:{evicted}次"],
111
+ "{count} succeeded": ["{count} succeeded", "成功:{count}次"],
112
+ "NOT INITIALIZED": ["UNCONFIRMED", "尚未确认"],
113
+ "active {active} / failed {failed} / delivery failures {delivery}": ["Active: {active} · failed: {failed}\nDelivery failures: {delivery}", "进行中:{active} · 生成失败:{failed}\n交付失败:{delivery}"],
114
+ "Setup state unknown / ccdx image-status": ["Check setup:\nccdx image-status", "查看设置:\nccdx image-status"],
115
+ "Snapshot {time}": ["Status updated {time}", "状态更新:{time}"],
116
+ "Status unavailable / check ccdx, then refresh": ["Cannot read status. Check ccdx, then refresh.", "状态读取失败,请检查后刷新。"],
117
+ "{count} retained / requests + events": ["{count} records retained", "保留:{count}条"],
118
+ "No recent request failures recorded.": ["No recent issues recorded.", "暂未记录异常。"],
119
+ "Request ended without successful completion.": ["This request did not finish successfully.", "该请求未能完整完成。"],
120
+ "{detail} / {retry}": ["{detail} / {retry}", "{detail} / {retry}"],
121
+ "retry attempted": ["retried", "已重试"],
122
+ "no retry": ["not retried", "未重试"],
123
+ "time": ["Time", "时间"],
124
+ "model": ["Model", "模型"],
125
+ "event": ["Event type", "事件类型"],
126
+ "code": ["Error code", "错误代码"],
127
+ "message": ["Error details", "错误详情"],
128
+ "response_id": ["Response ID", "回答编号"],
129
+ "upstream_request_id": ["Provider request ID", "服务编号"],
130
+ "request_id": ["Local request ID", "本机编号"],
131
+ "outcome": ["Result", "处理结果"],
132
+ "origin": ["Reported by", "结果来源"],
133
+ "phase": ["Last step", "最后步骤"],
134
+ "http_status": ["HTTP status", "返回状态"],
135
+ "upstream_attempts": ["Send attempts", "发送次数"],
136
+ "timings_ms": ["Timeline (ms)", "时间记录(毫秒)"],
137
+ "retry": ["Retry status", "重试情况"],
138
+ "retry_policy": ["Retry rule", "重试规则"],
139
+ "retry_skipped": ["Skip reason", "跳过原因"],
140
+ "attempted (outcome not recorded here)": ["Retried; result not recorded here.", "已重试;这里未记录结果。"],
141
+ "attempted": ["retried", "已重试"],
142
+ "not attempted": ["not retried", "未重试"],
143
+ "Copy diagnostic": ["Copy details", "复制详情"],
144
+ "Copied": ["Copied", "已复制"],
145
+ "Copy unavailable": ["Copy failed", "复制失败"],
146
+ "DEFAULT": ["DEFAULT", "默认"],
147
+ "Disabled by CCDX_TERMINAL_ANIMATION; saved choice applies after override removal and next start.": ["Disabled by CCDX_TERMINAL_ANIMATION.\nRemove this setting, then restart to apply your saved choice.", "启动设置已关闭动效。\nCCDX_TERMINAL_ANIMATION\n移除此设置后,重启生效。"],
148
+ "Saved choice applies on next ccdx start.": ["Saved choice applies after restarting ccdx.", "保存后,下次启动生效。"],
149
+ "CURRENT {theme}{unsaved}": ["SAVED {theme}{unsaved}", "已保存:{theme}{unsaved}"],
150
+ " / UNSAVED": [" / UNSAVED", " / 待保存"],
151
+ "Saving…": ["Saving…", "保存中"],
152
+ "Not saved / {error}": ["Not saved: {error}", "保存失败:{error}"],
153
+ "preview": ["preview", "测试版"],
154
+ "No selectable GPT models advertised.": ["No selectable GPT models found.", "暂未发现可选GPT模型。"],
155
+ "LIVE SNAPSHOT": ["FETCHED", "已获取"],
156
+ "{selectable} selectable / {advertised} advertised / {host} / {time}": ["Selectable: {selectable} · listed: {advertised}\nSource: {host} · checked {time}", "可选:{selectable}个 · 服务列出:{advertised}个\n来源:{host} · 获取时间:{time}"],
157
+ "LIVE LOOKUP FAILED": ["CHECK FAILED", "查询失败"],
158
+ "No live catalog available.": ["Cannot get the model list.", "无法获取模型列表。"],
159
+ "No usage records.": ["No usage records yet.", "暂未记录用量。"],
160
+ "LOCAL LOG": ["LOCAL RECORDS", "本机记录"],
161
+ "{records} records / {shown} of {models} models shown{day}": ["Records: {records} · models: {shown} of {models}{day}", "记录:{records}条\n模型:展示{shown}个,共{models}个{day}"],
162
+ " / {zone} / {model}": [" / {zone} / {model}", " / {zone} / {model}"],
163
+ "no retained history": ["no records", "没有记录"],
164
+ "{calls} recorded calls / input {input} / output {output} / cached {cached} (included in input){partial}": ["{calls} recorded requests\nInput: {input} · output: {output} · reused: {cached} Token\nReused input is included in input usage.{partial}", "请求:{calls}次\n输入用量:{input} Token\n输出用量:{output} Token\n重用输入:{cached} Token\n重用部分已计入输入用量。{partial}"],
165
+ " / partial token counts": ["\nIncomplete usage records.", "\n用量记录不完整。"],
166
+ "{date} / {description}": ["{date} / {description}", "{date} / {description}"],
167
+ "{metric} / PEAK {peak}": ["365 DAYS · {metric} · PEAK {peak}", "近365天 · {metric} · 峰值{peak}"],
168
+ "INPUT + OUTPUT": ["TOTAL USAGE (Token)", "总用量"],
169
+ "RECORDED CALLS": ["RECORDED REQUESTS", "请求次数"],
170
+ "{zone} / {from} — {to} / retained history starts {first}; oldest day may be partial.{excluded}{limit}": ["{zone} · {from} — {to}\nEarliest record: {first}. That day may be incomplete.{excluded}{limit}", "时区:{zone}\n记录范围:{from}—{to}\n最早记录:{first}\n最早一天的记录可能不全。{excluded}{limit}"],
171
+ " {count} records with invalid/future timestamps excluded.": ["\nSkipped {count} records with invalid or future dates.", "\n已跳过{count}条时间异常记录。"],
172
+ " Model filters limited to 100; all-model totals include the remainder.": ["\nFilters show up to 100 models. Totals include the rest.", "\n只列出前100个模型。\n总计包含其余模型。"],
173
+ "Selected {date} / {model}: totals in the Usage table above / All history resets the table.": ["Selected: {date} / {model}. See the usage table above.\nChoose Show all to reset the date.", "已选:{date}/{model}\n当天用量见上方表格。\n点击“查看全部”恢复总表。"],
174
+ "Select a day for model totals. Calls are usage records, not messages or time. Hatched cells: no retained history; empty cells: zero recorded calls.": ["Select a day to view its usage.\nCounts are recorded requests, not messages or time.\nHatched: no records. Blank: zero recorded requests.", "点击日期,查看当天用量。\n按已记录的请求统计。\n不代表消息数或使用时长。\n斜线格表示没有记录。\n空白格表示已记录零次请求。"],
175
+ "LOG UNAVAILABLE": ["READ FAILED", "读取失败"],
176
+ "No usage summary available.": ["Cannot read usage records.", "无法读取用量记录。"],
177
+ "Analytics unavailable / refresh Usage to retry.": ["Trends unavailable. Refresh Model usage to retry.", "统计失败,请刷新用量。"],
178
+ "Animation settings are invalid; inspect them with ccdx animation": ["Invalid animation settings.\nRun ccdx animation.", "设置无效,请检查动效。\n运行ccdx animation。"],
179
+ "Animation setting was not saved; check ccdx animation": ["Animation was not saved.\nRun ccdx animation.", "保存失败,请检查动效。\n运行ccdx animation。"],
180
+ "Could not read local usage summary": ["Cannot read usage. Refresh to retry.", "无法读取用量,请刷新。"],
181
+ "Live model lookup timed out. Retry or run ccdx models.": ["Model check timed out. Try again.\nRun ccdx models for details.", "模型查询超时,请重试。\n运行ccdx models。"],
182
+ "GitHub token not found. Start ccdx to sign in.": ["No GitHub sign-in details found.\nRun ccdx to sign in.", "未找到登录信息。\n运行ccdx登录。"],
183
+ "Live model lookup failed. Run ccdx models for details.": ["Model check failed.\nRun ccdx models for details.", "模型查询失败,请查看详情。\n运行ccdx models。"],
184
+ "Live model lookup failed (HTTP {status}). Run ccdx models for details.": ["Model check failed: HTTP {status}.\nRun ccdx models for details.", "模型查询失败:HTTP {status}\n运行ccdx models。"],
185
+ "Select a listed animation theme": ["Choose an animation from the list.", "请选择列表中的动效。"],
186
+ "Animation changes require the same-origin dashboard": ["Save from this computer's dashboard.", "请从本机看板保存动效。"],
187
+ "Animation changes require application/json": ["The save format is invalid.", "保存内容格式不正确。"],
188
+ "Invalid animation request body": ["The animation selection is invalid.", "动效选择格式不正确。"],
189
+ "Select a valid usage time zone": ["Choose a valid time zone.", "所选时区无效。"],
190
+ "Method not allowed": ["This action is not supported.", "此操作不受支持。"],
191
+ "Dashboard API is available only at a loopback address": ["Open the dashboard on this computer.", "请在本机打开看板。"],
192
+ "Dashboard API requires a same-origin page request": ["Use this computer's dashboard to continue.", "请从本机看板操作。"],
193
+ "{time} / {model} / {code}": ["{time} / {model} / {code}", "{time} / {model} / {code}"],
194
+ "unknown time": ["unknown time", "时间未知"],
195
+ "unknown_model": ["unknown model", "模型未知"],
196
+ "unknown_error": ["unknown error", "错误未知"],
197
+ "{index} {theme}": ["{index} {theme}", "{index} {theme}"],
198
+ "Comet": ["Comet", "彗星"],
199
+ "Twin": ["Twin", "双星"],
200
+ "Shuttle": ["Shuttle", "往返"],
201
+ "Chase": ["Chase", "追逐"],
202
+ "Mirror": ["Mirror", "对称"],
203
+ "Pulse": ["Pulse", "呼吸"],
204
+ "Stack": ["Stack", "堆叠"],
205
+ "Relay": ["Relay", "接力"],
206
+ "Split": ["Split", "分岔"],
207
+ "COMET": ["COMET", "彗星"],
208
+ "TWIN": ["TWIN", "双星"],
209
+ "SHUTTLE": ["SHUTTLE", "往返"],
210
+ "CHASE": ["CHASE", "追逐"],
211
+ "MIRROR": ["MIRROR", "对称"],
212
+ "PULSE": ["PULSE", "呼吸"],
213
+ "STACK": ["STACK", "堆叠"],
214
+ "RELAY": ["RELAY", "接力"],
215
+ "SPLIT": ["SPLIT", "分岔"],
183
216
  };
184
217
  const storageKey = "ccdx.dashboard.language";
185
218
  let language = "en";
@@ -189,7 +222,7 @@
189
222
  const attributes = { textContent: "data-i18n", title: "data-i18n-title", "aria-label": "data-i18n-label" };
190
223
 
191
224
  function format(key, values = {}) {
192
- const template = language === "zh" ? translations[key] || key : key;
225
+ const template = Object.hasOwn(translations, key) ? translations[key][language === "zh" ? 1 : 0] : key;
193
226
  return String(template).replace(/\{(\w+)\}/g, (match, name) => {
194
227
  if (!Object.hasOwn(values, name)) return match;
195
228
  const value = values[name];
@@ -219,7 +252,7 @@
219
252
  }
220
253
  const button = document.getElementById("language-toggle");
221
254
  button.textContent = language === "en" ? "中文" : "EN";
222
- const label = language === "en" ? "Switch to Chinese" : "切换为英文";
255
+ const label = language === "en" ? "Switch to Chinese" : "切换英文";
223
256
  button.setAttribute("aria-label", label);
224
257
  button.title = label;
225
258
  }
@@ -72,6 +72,7 @@ h1 span { color: var(--accent-soft); }
72
72
  .section-actions { display: flex; align-items: center; gap: 15px; margin-top: 16px; }
73
73
  .section-actions span { color: var(--muted); font-size: 11px; }
74
74
  .table-note { margin: 15px 0 8px; color: var(--muted); font-size: 11px; }
75
+ .card p, .hint, .table-note, .section-actions span, .chart-heading span { white-space: pre-line; }
75
76
  .table-wrap { width: 100%; overflow-x: auto; }
76
77
  table { width: 100%; border-collapse: collapse; font-size: 11px; }
77
78
  th { color: var(--heading); text-align: left; font-weight: 700; letter-spacing: .05em; }
@@ -81,7 +81,11 @@ function renderFailures(recent, recentRequests = []) {
81
81
  details.open = expanded.has(details.dataset.key);
82
82
  const title = document.createElement("summary");
83
83
  localize(title, "{detail} / {retry}", {
84
- detail: `${String(failure.at || "unknown time")} / ${String(failure.model || "unknown_model").slice(0, 80)} / ${String(failure.code || "unknown_error").slice(0, 80)}`,
84
+ detail: phrase("{time} / {model} / {code}", {
85
+ time: failure.at ? String(failure.at) : phrase("unknown time"),
86
+ model: failure.model ? String(failure.model).slice(0, 80) : phrase("unknown_model"),
87
+ code: failure.code ? String(failure.code).slice(0, 80) : phrase("unknown_error"),
88
+ }),
85
89
  retry: phrase(failure.retried ? "retry attempted" : "no retry"),
86
90
  });
87
91
  const timeline = failure.timeline;
@@ -311,7 +315,9 @@ function buildAnimationOptions(data) {
311
315
  });
312
316
  const name = document.createElement("span");
313
317
  name.className = "theme-name";
314
- name.textContent = `${String(index + 1).padStart(2, "0")} ${theme.label}`;
318
+ const label = document.createElement("span");
319
+ localize(label, "{index} {theme}", { index: String(index + 1).padStart(2, "0"), theme: phrase(theme.label) });
320
+ name.append(label);
315
321
  if (theme.default) {
316
322
  const marker = document.createElement("small");
317
323
  localize(marker, "DEFAULT");
@@ -365,7 +371,7 @@ async function loadAnimation() {
365
371
  }
366
372
 
367
373
  function updateAnimationState() {
368
- text("animation-state", "CURRENT {theme}{unsaved}", { theme: savedAnimation.toUpperCase(), unsaved: selectedAnimation !== savedAnimation ? phrase(" / UNSAVED") : "" });
374
+ text("animation-state", "CURRENT {theme}{unsaved}", { theme: phrase(savedAnimation.toUpperCase()), unsaved: selectedAnimation !== savedAnimation ? phrase(" / UNSAVED") : "" });
369
375
  }
370
376
 
371
377
  async function saveAnimation() {
@@ -435,7 +441,10 @@ async function loadModels() {
435
441
  text("models-note", "{selectable} selectable / {advertised} advertised / {host} / {time}", { selectable: number(data.selectable), advertised: number(data.advertised), host: data.upstream_host, time: new Date(data.checked_at).toLocaleTimeString() });
436
442
  } catch (error) {
437
443
  text("models-state", "LIVE LOOKUP FAILED");
438
- text("models-note", String(error.message || error).slice(0, 240));
444
+ const message = String(error.message || error).slice(0, 240);
445
+ const httpError = /^Live model lookup failed \(HTTP (\d{3})\)\. Run ccdx models for details\.$/.exec(message);
446
+ if (httpError) text("models-note", "Live model lookup failed (HTTP {status}). Run ccdx models for details.", { status: httpError[1] });
447
+ else text("models-note", message);
439
448
  emptyTable("live-models-body", 4, "No live catalog available.");
440
449
  } finally {
441
450
  button.disabled = false;
package/src/image-mcp.mjs CHANGED
@@ -2,7 +2,8 @@ import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
- import { isLoopbackAddress } from "./observability.mjs";
5
+ import { isIP } from "node:net";
6
+ import { isLoopbackAddress, isLoopbackHostHeader } from "./observability.mjs";
6
7
  import { generateImage, supportsImageEditing, IMAGE_MAX_BYTES } from "./image-provider.mjs";
7
8
  import { readImageProviderConfig } from "./image-provider-config.mjs";
8
9
  import { createImageReferenceStore } from "./image-references.mjs";
@@ -49,6 +50,28 @@ function sameHostSocket(socket) {
49
50
  return remote && local && remote === local;
50
51
  }
51
52
 
53
+ function canonicalIpHost(hostname) {
54
+ const address = hostname.replace(/^\[|\]$/g, "");
55
+ const mapped = /^::ffff:([a-f0-9]{1,4}):([a-f0-9]{1,4})$/.exec(address);
56
+ if (!mapped) return address;
57
+ const high = Number.parseInt(mapped[1], 16);
58
+ const low = Number.parseInt(mapped[2], 16);
59
+ return [high >> 8, high & 255, low >> 8, low & 255].join(".");
60
+ }
61
+
62
+ function ownHostHeader(host, socket, bindHostname) {
63
+ if (isLoopbackHostHeader(host)) return true;
64
+ if (typeof host !== "string" || !host || host.includes(",")) return false;
65
+ try {
66
+ const url = new URL(`http://${host}`);
67
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) return false;
68
+ if (bindHostname && url.hostname === bindHostname) return true;
69
+ const local = String(socket?.localAddress || "");
70
+ if (!isIP(local)) return false;
71
+ return canonicalIpHost(url.hostname) === canonicalIpHost(new URL(`http://${isIP(local) === 6 ? `[${local}]` : local}`).hostname);
72
+ } catch { return false; }
73
+ }
74
+
52
75
  function writeJson(res, statusCode, body) {
53
76
  if (res.destroyed || res.writableEnded) return;
54
77
  res.writeHead(statusCode, {
@@ -134,9 +157,14 @@ export function createImageMcpHandler({
134
157
  configLoader = () => readImageProviderConfig(),
135
158
  generateImageFn = generateImage,
136
159
  maxConcurrent = 2,
160
+ localHostname = "",
137
161
  imageReferences = createImageReferenceStore(),
138
162
  imageDirectory = path.join(os.homedir(), ".local", "share", "codex-copilot-dx", "images"),
139
163
  } = {}) {
164
+ let bindHostname = "";
165
+ if (localHostname && !isIP(localHostname.replace(/^\[|\]$/g, ""))) {
166
+ try { bindHostname = new URL(`http://${localHostname}`).hostname; } catch {}
167
+ }
140
168
  let active = 0;
141
169
  const metrics = { total: 0, succeeded: 0, failed: 0, busy: 0, cancelled: 0, delivery_failures: 0 };
142
170
  const handler = async function imageMcpHandler(req, res) {
@@ -144,6 +172,10 @@ export function createImageMcpHandler({
144
172
  writeJson(res, 403, jsonRpcError(null, -32001, "Image MCP is available only on this device"));
145
173
  return;
146
174
  }
175
+ if (!ownHostHeader(req.headers?.host, req.socket, bindHostname)) {
176
+ writeJson(res, 403, jsonRpcError(null, -32001, "Image MCP is available only on this device"));
177
+ return;
178
+ }
147
179
  if (req.method !== "POST") {
148
180
  res.setHeader?.("Allow", "POST");
149
181
  writeJson(res, 405, jsonRpcError(null, -32600, "Only POST is supported"));
@@ -207,10 +207,11 @@ function publicAddress(address) {
207
207
  catch { return false; }
208
208
  if (lower === "::" || lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd")
209
209
  || /^fe[89ab]/.test(lower) || lower.startsWith("ff") || lower.startsWith("2001:db8:")) return false;
210
- const mapped = /^::ffff:([a-f0-9]{1,4}):([a-f0-9]{1,4})$/.exec(lower);
210
+ const mapped = /^::ffff:([a-f0-9]{1,4}):([a-f0-9]{1,4})$/.exec(lower)
211
+ || /^64:ff9b::(?:([a-f0-9]{1,4}):)?([a-f0-9]{1,4})?$/.exec(lower);
211
212
  if (!mapped) return true;
212
- const high = Number.parseInt(mapped[1], 16);
213
- const low = Number.parseInt(mapped[2], 16);
213
+ const high = Number.parseInt(mapped[1] || "0", 16);
214
+ const low = Number.parseInt(mapped[2] || "0", 16);
214
215
  return ipv4Public([high >> 8, high & 255, low >> 8, low & 255].join("."));
215
216
  }
216
217
 
@@ -305,20 +306,36 @@ export async function generateImage(config, {
305
306
  const timeout = AbortSignal.timeout(timeoutMs);
306
307
  const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
307
308
  let response;
308
- try {
309
- response = await fetchImpl(config.endpoint, {
310
- method: "POST",
311
- headers: {
312
- Authorization: `Bearer ${config.api_key}`,
313
- "Content-Type": "application/json",
314
- Accept: "application/json",
315
- },
316
- body: JSON.stringify(generationBody(config, normalizedPrompt, normalizedSize, normalizedImage)),
317
- signal: requestSignal,
318
- });
319
- } catch (error) {
320
- if (requestSignal.aborted) throw providerError("Image generation timed out or was cancelled", "ccdx_image_timeout");
321
- throw providerError("Image API could not be reached");
309
+ let requestUrl = config.endpoint;
310
+ let method = "POST";
311
+ let requestBody = JSON.stringify(generationBody(config, normalizedPrompt, normalizedSize, normalizedImage));
312
+ let headers = { Authorization: `Bearer ${config.api_key}`, "Content-Type": "application/json", Accept: "application/json" };
313
+ for (let redirects = 0; ; redirects += 1) {
314
+ try {
315
+ if (requestSignal.aborted) throw requestSignal.reason;
316
+ response = await fetchImpl(requestUrl, { method, headers, body: requestBody, signal: requestSignal, redirect: "manual" });
317
+ } catch (error) {
318
+ if (requestSignal.aborted) throw providerError("Image generation timed out or was cancelled", "ccdx_image_timeout");
319
+ throw providerError("Image API could not be reached");
320
+ }
321
+ const location = [301, 302, 303, 307, 308].includes(response.status) ? response.headers?.get?.("location") : null;
322
+ if (location == null) break;
323
+ await response.body?.cancel?.().catch?.(() => {});
324
+ let target;
325
+ try { target = new URL(location, requestUrl); }
326
+ catch { throw providerError("Image API returned an invalid redirect", "ccdx_image_redirect_unsafe"); }
327
+ if (target.protocol !== "https:" || target.username || target.password || target.origin !== new URL(requestUrl).origin) {
328
+ throw providerError("Image API redirect was refused; use a same-origin HTTPS endpoint", "ccdx_image_redirect_unsafe");
329
+ }
330
+ if (redirects >= 20) throw providerError("Image API returned too many redirects", "ccdx_image_redirect_limit");
331
+ // Preserve Fetch's method/body rules for legitimate same-origin redirects.
332
+ if (((response.status === 301 || response.status === 302) && method === "POST") || (response.status === 303 && method !== "GET")) {
333
+ method = "GET";
334
+ requestBody = undefined;
335
+ headers = { ...headers };
336
+ delete headers["Content-Type"];
337
+ }
338
+ requestUrl = target.href;
322
339
  }
323
340
  const body = await boundedJson(response);
324
341
  if (!response.ok) {
@@ -233,6 +233,9 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
233
233
  delete chatReq.max_tokens;
234
234
  const requestModel = chatReq.model || "unknown";
235
235
  let upstreamReq = withChatStreamUsage(chatReq);
236
+ // Retain names only, not the request's schemas, images or message history.
237
+ const declaredToolNames = new Set((Array.isArray(upstreamReq.tools) ? upstreamReq.tools : [])
238
+ .map((tool) => tool?.function?.name).filter((name) => typeof name === "string" && name));
236
239
  let {
237
240
  abort,
238
241
  bodyText,
@@ -450,7 +453,17 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
450
453
  for (const tc of delta.tool_calls) {
451
454
  const { tool, created } = await ensureToolCall(tc);
452
455
  if (tc.id) tool.callId = tc.id;
453
- if (!created && tc.function?.name) tool.name += tc.function.name;
456
+ if (!created && tc.function?.name) {
457
+ const name = tc.function.name;
458
+ let completeRepeat = name === tool.name && declaredToolNames.has(name);
459
+ if (completeRepeat) {
460
+ // An overlap may be a real fragment of a longer declared name.
461
+ for (const declared of declaredToolNames) {
462
+ if (declared.startsWith(tool.name + name)) { completeRepeat = false; break; }
463
+ }
464
+ }
465
+ if (!completeRepeat) tool.name += name;
466
+ }
454
467
  const argumentDelta = tc.function?.arguments;
455
468
  toolArgumentGuard.observe(tool.outputIndex, argumentDelta);
456
469
  if (argumentDelta) {