ai-zero-token 2.0.3 → 2.0.5

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +34 -2
  3. package/README.zh-CN.md +35 -3
  4. package/admin-ui/dist/assets/{accounts-DymL4WIa.js → accounts-ABMyXo4H.js} +3 -1
  5. package/admin-ui/dist/assets/{docs-DtO-AOWU.js → docs-Dh0aFha_.js} +3 -3
  6. package/admin-ui/dist/assets/{image-bed-yIVQ4dKs.js → image-bed-C1M7-0q1.js} +1 -1
  7. package/admin-ui/dist/assets/index--rNjdmzf.js +10 -0
  8. package/admin-ui/dist/assets/{index-By4r-wy3.css → index-DjtN30PC.css} +1 -1
  9. package/admin-ui/dist/assets/{launch-CQXYrl-h.js → launch-pB7YlWFI.js} +1 -1
  10. package/admin-ui/dist/assets/logs-B7McijSi.js +1 -0
  11. package/admin-ui/dist/assets/{network-detect-sSrnwZqf.js → network-detect-Bx3XmXPk.js} +1 -1
  12. package/admin-ui/dist/assets/{overview-BbSON0Jl.js → overview-CV0H2Nsq.js} +1 -1
  13. package/admin-ui/dist/assets/settings-ynCIdUvZ.js +7 -0
  14. package/admin-ui/dist/assets/{tester-CftPgRE9.js → tester-BG-up8qP.js} +1 -1
  15. package/admin-ui/dist/index.html +2 -2
  16. package/build/tray-icon-template.png +0 -0
  17. package/dist/core/providers/http-client.js +228 -3
  18. package/dist/core/providers/openai-codex/chat.js +160 -23
  19. package/dist/core/services/auth-service.js +14 -5
  20. package/dist/core/services/chat-service.js +1 -0
  21. package/dist/core/services/config-service.js +15 -5
  22. package/dist/core/store/codex-auth-store.js +295 -4
  23. package/dist/core/store/settings-store.js +54 -24
  24. package/dist/desktop/main.js +616 -15
  25. package/dist/server/app.js +859 -91
  26. package/dist/server/index.js +2 -1
  27. package/docs/API_USAGE.md +82 -1
  28. package/docs/DESKTOP_RELEASE.md +24 -0
  29. package/package.json +3 -1
  30. package/admin-ui/dist/assets/index-DRe-tByu.js +0 -10
  31. package/admin-ui/dist/assets/logs-awABDg1C.js +0 -1
  32. package/admin-ui/dist/assets/settings-DvRiHS7i.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.5 - 2026-05-09
4
+
5
+ - Added Codex custom provider setup from the Settings page, including local/remote gateway URL selection and managed writes to `~/.codex/config.toml`.
6
+ - Added `POST /codex/v1/responses` as a dedicated Codex CLI/Desktop Responses SSE passthrough route.
7
+ - Added admin APIs to configure or remove the AI Zero Token managed Codex provider and report provider status in the management console.
8
+ - Added an auto-switch exclusion list so selected accounts can be kept out of automatic quota rotation while remaining available for manual use.
9
+ - Improved quota-limit handling by capturing upstream `usage_limit_reached` details and retrying Codex passthrough requests after automatic account switching.
10
+ - Hardened settings persistence with normalized settings loading, deduplicated profile ID lists, queued saves, and atomic file replacement.
11
+ - Updated README and API docs with Codex custom provider setup and the dedicated passthrough route.
12
+
13
+ ## 2.0.4 - 2026-05-08
14
+
15
+ - Added the macOS menu-bar account panel for quick gateway/Codex account switching, quota refresh, Base URL copy, and gateway restart.
16
+ - Added OpenClaw-oriented `chat.completions` compatibility for `tools`, `tool_choice`, assistant `tool_calls`, tool-role follow-up messages, `reasoning_effort`, `parallel_tool_calls`, and `stream=true` SSE responses.
17
+ - Added gateway request logs backed by real API traffic, including safe request/response summaries and OpenClaw source detection.
18
+ - Added desktop support for restarting Codex after applying a saved account.
19
+ - Added the tray template icon to npm and Electron desktop package resources.
20
+ - Updated API docs and in-app usage docs with OpenClaw setup and streaming/tool-call compatibility notes.
21
+
3
22
  ## 2.0.3 - 2026-05-08
4
23
 
5
24
  - Added ZIP batch account import with preflight validation for bundled account JSON files.
package/README.md CHANGED
@@ -24,6 +24,7 @@ AI Zero Token provides a local CLI, web console, and HTTP gateway that expose sa
24
24
  - Optional quota-exhaustion auto switch to the next saved API account with available quota, plus configurable quota refresh concurrency.
25
25
  - Optional upstream proxy configuration for OAuth, model refresh, and gateway forwarding.
26
26
  - Local model discovery from the Codex model cache with manual refresh support.
27
+ - OpenClaw-oriented chat compatibility for streaming, tool calls, tool result messages, and request-log diagnostics.
27
28
 
28
29
  ## Architecture
29
30
 
@@ -79,6 +80,8 @@ For local clients, keep using:
79
80
  http://127.0.0.1:8787/v1
80
81
  ```
81
82
 
83
+ On macOS, the desktop app also adds a menu-bar panel for quick account switching. The panel can switch the active gateway account, apply an account to local Codex, refresh quota status, copy the API Base URL, and restart the local gateway.
84
+
82
85
  ## Web Console
83
86
 
84
87
  The web console is the recommended entry point:
@@ -129,6 +132,34 @@ curl http://127.0.0.1:8787/v1/chat/completions \
129
132
  }'
130
133
  ```
131
134
 
135
+ For OpenClaw or other OpenAI-compatible coding clients, configure:
136
+
137
+ ```text
138
+ Provider: OpenAI compatible
139
+ Base URL: http://127.0.0.1:8787/v1
140
+ API Key: local
141
+ Model: gpt-5.4
142
+ Streaming: enabled
143
+ Tools / function calling: enabled
144
+ ```
145
+
146
+ `/v1/chat/completions` accepts `stream=true`, `tools`, `tool_choice`, `parallel_tool_calls`, assistant `tool_calls`, tool-role result messages, and `reasoning_effort`. OpenClaw requests also appear in the management console request log with safe summaries.
147
+
148
+ Codex CLI/Desktop can also use the gateway as a custom Responses provider:
149
+
150
+ Use the management console Settings page and click "接管 Codex 请求" to write this config automatically. Click "解除接管" to remove the AI Zero Token managed provider config, or add it manually to `~/.codex/config.toml`:
151
+
152
+ ```toml
153
+ model = "gpt-5.4"
154
+ model_provider = "ai-zero-token"
155
+
156
+ [model_providers.ai-zero-token]
157
+ name = "AI Zero Token"
158
+ base_url = "http://127.0.0.1:8787/codex/v1"
159
+ wire_api = "responses"
160
+ supports_websockets = false
161
+ ```
162
+
132
163
  ### Image Generation
133
164
 
134
165
  ```bash
@@ -233,7 +264,7 @@ The persistent state directory can be overridden with:
233
264
  AI_ZERO_TOKEN_HOME=/path/to/home azt start
234
265
  ```
235
266
 
236
- The web console settings are persisted in the same local state directory. The quota auto-switch option is stored as `autoSwitch.enabled`; when enabled, the gateway uses the latest saved quota snapshot and moves API traffic away from the active account once that snapshot shows a quota window is exhausted.
267
+ The web console settings are persisted in the same local state directory. The quota auto-switch option is stored as `autoSwitch.enabled`; when enabled, the gateway uses the latest saved quota snapshot and moves API traffic away from the active account once that snapshot shows a quota window is exhausted. Accounts listed in `autoSwitch.excludedProfileIds` are excluded from automatic rotation and remain available for manual selection.
237
268
 
238
269
  The global quota refresh concurrency can be configured in the web console settings. The default is `16`; lower it if upstream rate limits increase, or raise it for larger local account pools.
239
270
 
@@ -254,7 +285,8 @@ For JSON image editing, base64 payloads are about 33% larger than the original i
254
285
  ## Limitations
255
286
 
256
287
  - This project is intended for local single-user use.
257
- - Streaming request fields are recognized, but full streaming parity is not implemented for every endpoint.
288
+ - `/v1/chat/completions` supports OpenAI-style SSE for `stream=true`; Codex custom providers use the dedicated `/codex/v1/responses` route for upstream Responses SSE passthrough.
289
+ - `/v1/chat/completions` supports common tool/function-calling fields, but `n > 1` is not supported.
258
290
  - `/v1/images/generations` currently returns `b64_json`; hosted image URLs are not supported.
259
291
  - `/v1/images/generations` does not support `n > 1`.
260
292
  - `/v1/images/edits` currently supports JSON only. `multipart/form-data`, `mask`, and `file_id` are not yet supported.
package/README.zh-CN.md CHANGED
@@ -24,6 +24,7 @@ AI Zero Token 是一个本地优先的 OpenAI 兼容网关,用于把 ChatGPT/C
24
24
  - 支持 API 账号额度耗尽后自动切换到下一个仍有额度的账号,并可配置全局额度刷新并发数。
25
25
  - 支持上游代理配置,覆盖 OAuth、模型刷新和接口转发。
26
26
  - 模型列表优先读取本机 Codex 模型缓存,并支持手动刷新。
27
+ - 面向 OpenClaw 增强 Chat Completions 兼容,支持流式、工具调用、工具结果消息和请求日志诊断。
27
28
 
28
29
  ## 技术架构
29
30
 
@@ -79,6 +80,8 @@ npm run dist:win
79
80
  http://127.0.0.1:8787/v1
80
81
  ```
81
82
 
83
+ macOS 桌面端还会常驻菜单栏,提供快速账号面板。可以从菜单栏切换网关账号、把账号应用到本机 Codex、刷新额度、复制 API Base URL 和重启本地网关。
84
+
82
85
  ## 管理页
83
86
 
84
87
  管理页是推荐入口,可以完成:
@@ -90,7 +93,7 @@ http://127.0.0.1:8787/v1
90
93
  - 勾选后批量删除账号。
91
94
  - 将已保存账号应用到本机 Codex。
92
95
  - 配置默认文本模型和上游代理。
93
- - 开启当前 API 账号额度耗尽后的自动切换。
96
+ - 开启当前 API 账号额度耗尽后的自动切换,并配置不参与自动轮换的账号名单。
94
97
  - 为账号池较多的场景调整全局额度刷新并发数。
95
98
  - 测试 `models`、`responses`、`chat.completions`、`images.generations`、`images.edits`。
96
99
  - 当检测到新版本时显示全局顶部更新提示,分别引导桌面端下载 GitHub Release 和 npm 用户更新包。
@@ -129,6 +132,34 @@ curl http://127.0.0.1:8787/v1/chat/completions \
129
132
  }'
130
133
  ```
131
134
 
135
+ OpenClaw 或其他 OpenAI 兼容编程客户端可以这样配置:
136
+
137
+ ```text
138
+ Provider: OpenAI compatible
139
+ Base URL: http://127.0.0.1:8787/v1
140
+ API Key: local
141
+ Model: gpt-5.4
142
+ Streaming: enabled
143
+ Tools / function calling: enabled
144
+ ```
145
+
146
+ `/v1/chat/completions` 支持 `stream=true`、`tools`、`tool_choice`、`parallel_tool_calls`、assistant `tool_calls`、tool role 结果消息和 `reasoning_effort`。OpenClaw 请求也会在管理页请求日志里显示,并只保存安全摘要。
147
+
148
+ Codex CLI/Desktop 也可以把本工具配置成自定义 Responses provider:
149
+
150
+ 管理页“系统设置”里可以直接点击“接管 Codex 请求”写入该配置;点击“解除接管”会移除 AI Zero Token 管理的 provider 配置。也可以手动写入 `~/.codex/config.toml`:
151
+
152
+ ```toml
153
+ model = "gpt-5.4"
154
+ model_provider = "ai-zero-token"
155
+
156
+ [model_providers.ai-zero-token]
157
+ name = "AI Zero Token"
158
+ base_url = "http://127.0.0.1:8787/codex/v1"
159
+ wire_api = "responses"
160
+ supports_websockets = false
161
+ ```
162
+
132
163
  ### 文生图
133
164
 
134
165
  ```bash
@@ -233,7 +264,7 @@ AZT_CORS_ORIGIN=http://127.0.0.1:8124,http://localhost:3000 azt start
233
264
  AI_ZERO_TOKEN_HOME=/path/to/home azt start
234
265
  ```
235
266
 
236
- 管理页里的配置会保存在同一个本地状态目录。额度耗尽自动切换会保存为 `autoSwitch.enabled`;开启后,网关会根据最近一次保存的额度快照判断当前 API 账号是否耗尽,并把 API 流量切到下一个仍有额度的账号。
267
+ 管理页里的配置会保存在同一个本地状态目录。额度耗尽自动切换会保存为 `autoSwitch.enabled`;开启后,网关会根据最近一次保存的额度快照判断当前 API 账号是否耗尽,并把 API 流量切到下一个仍有额度的账号。`autoSwitch.excludedProfileIds` 里的账号不参与自动轮换,但仍可手动应用。
237
268
 
238
269
  全局额度刷新并发数可以在管理页设置里调整,默认 `16`。账号很多时可以调高;遇到上游限流或失败增多时建议调低。
239
270
 
@@ -254,7 +285,8 @@ ChatGPT Images 的可用性和额度由上游账号决定。Free 账号可以尝
254
285
  ## 当前限制
255
286
 
256
287
  - 项目默认面向本地单用户使用。
257
- - `stream=true` 目前只识别,并未对所有接口实现完整流式兼容。
288
+ - `/v1/chat/completions` 已支持 OpenAI 风格 SSE 流式;Codex 自定义 provider 使用 `/codex/v1/responses` 专用路由做上游 Responses SSE 透传。
289
+ - `/v1/chat/completions` 支持常见工具/函数调用字段,但暂不支持 `n > 1`。
258
290
  - `/v1/images/generations` 当前返回 `b64_json`,暂不支持托管图片 URL。
259
291
  - `/v1/images/generations` 暂不支持 `n > 1`。
260
292
  - `/v1/images/edits` 当前只支持 JSON,暂不支持 `multipart/form-data`、`mask` 和 `file_id`。
@@ -1,2 +1,4 @@
1
- import{a as e,r as t,t as n}from"./jsx-runtime-DqpGtLhh.js";import{t as r}from"./earth-DFdZaQIi.js";import{t as i}from"./refresh-cw-CAAH2rqe.js";import{t as a}from"./search-B2hz41D3.js";import{C as o,_ as s,a as c,b as l,d as u,f as d,g as f,h as p,i as m,m as h,n as g,o as _,p as v,r as y,s as b,t as x,u as S,v as C,w,y as T}from"./profiles-DMOjJORP.js";import{_ as E,d as D,p as O,r as k,x as A}from"./index-DRe-tByu.js";import{t as j}from"./InfoRow-0ULI9iI3.js";var M=e(t(),1),N=n();function P(e){let t=e.config?.codex?.accountId,n=e.profiles.length<=0?``:e.profiles.length===1?`profile-count-1`:e.profiles.length===2?`profile-count-2`:e.profiles.length===3?`profile-count-3`:`profile-count-many`;return(0,N.jsxs)(`section`,{className:`card`,id:`accounts`,children:[(0,N.jsxs)(`div`,{className:`section-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`h2`,{children:`账号额度预览`}),(0,N.jsx)(`p`,{children:`账号信息采用卡片式布局展示,支持搜索、状态筛选和额度排序。`})]}),(0,N.jsxs)(`div`,{className:`section-actions`,children:[(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onLocate,children:`定位当前账号`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onExportSelected,children:`导出所选`}),(0,N.jsx)(`button`,{className:`btn-danger`,type:`button`,onClick:e.onRemoveSelected,disabled:e.selectedCount===0||e.busy===`bulk-remove`,children:`删除所选`}),(0,N.jsx)(`button`,{className:`btn-primary`,type:`button`,onClick:e.onAddAccount,children:`新增账号`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onRefreshStatus,children:`刷新状态`}),(0,N.jsx)(`button`,{className:`btn-danger`,type:`button`,onClick:e.onClearAccounts,children:`清空账号`})]})]}),(0,N.jsxs)(`div`,{className:`filter-row`,children:[(0,N.jsxs)(`label`,{className:`search-box`,children:[(0,N.jsx)(a,{size:16}),(0,N.jsx)(`input`,{value:e.filter.search,onChange:t=>e.onFilter({...e.filter,search:t.target.value}),placeholder:`搜索邮箱、账号 ID 或 Profile ID`})]}),(0,N.jsxs)(`select`,{className:`control`,value:e.filter.status,onChange:t=>e.onFilter({...e.filter,status:t.target.value}),children:[(0,N.jsx)(`option`,{value:`all`,children:`全部状态`}),(0,N.jsx)(`option`,{value:`active`,children:`使用中`}),(0,N.jsx)(`option`,{value:`healthy`,children:`健康`}),(0,N.jsx)(`option`,{value:`warning`,children:`即将耗尽`}),(0,N.jsx)(`option`,{value:`exhausted`,children:`额度耗尽`}),(0,N.jsx)(`option`,{value:`invalid`,children:`登录失效`}),(0,N.jsx)(`option`,{value:`expired`,children:`已过期`})]}),(0,N.jsxs)(`select`,{className:`control`,value:e.filter.sort,onChange:t=>e.onFilter({...e.filter,sort:t.target.value}),children:[(0,N.jsx)(`option`,{value:`quota-desc`,children:`默认排序`}),(0,N.jsx)(`option`,{value:`latency-asc`,children:`按额度更新时间`}),(0,N.jsx)(`option`,{value:`expiry-asc`,children:`按过期时间`}),(0,N.jsx)(`option`,{value:`name-asc`,children:`按名称排序`}),(0,N.jsx)(`option`,{value:`quota-asc`,children:`按剩余额度升序`}),(0,N.jsx)(`option`,{value:`plan-desc`,children:`按套餐排序`}),(0,N.jsx)(`option`,{value:`email-asc`,children:`按邮箱排序`})]}),(0,N.jsxs)(`span`,{className:`account-selected-count`,children:[`已选择 `,e.selectedCount,` 个`]})]}),(0,N.jsx)(`div`,{className:`account-grid ${n}`,children:e.profiles.length===0?(0,N.jsx)(`div`,{className:`empty-state`,children:`还没有匹配的账号。可以新增账号或调整筛选条件。`}):e.profiles.map(n=>{let a=d(n),o=u(n),p=T(n),y=!!e.expandedProfiles[n.profileId],b=!!(t&&n.accountId===t),S=l(n,b),w=_(n),D=c(n),O=n.exportAudit,k=O?.exported?`已导出 ${O.count} 次`:`未导出`,M=typeof e.busy==`string`&&e.busy.startsWith(`profile:`)&&e.busy.endsWith(n.profileId),P=e.busy===`profile:sync-quota:${n.profileId}`;return(0,N.jsxs)(`article`,{className:`account-card plan-${g(n)} ${w?`is-auth-invalid`:``}`,"data-profile-card":n.profileId,title:w?x(n):void 0,children:[S&&(0,N.jsx)(`span`,{className:`usage-corner ${S.className}`,children:(0,N.jsx)(`span`,{children:S.label})}),(0,N.jsxs)(`div`,{className:`account-head`,children:[(0,N.jsxs)(`div`,{className:`account-title`,children:[(0,N.jsxs)(`div`,{className:`account-name`,children:[(0,N.jsx)(`span`,{className:`avatar`,children:v(n)}),(0,N.jsx)(`strong`,{children:h(n,e.showEmails)}),(0,N.jsx)(`button`,{"aria-label":`刷新额度`,className:`account-icon-btn`,disabled:M,onClick:()=>e.onAction(`sync-quota`,n),title:`刷新额度`,type:`button`,children:P?(0,N.jsx)(E,{className:`spin`,size:14}):(0,N.jsx)(i,{size:14})})]}),(0,N.jsxs)(`div`,{className:`badge-row`,children:[(0,N.jsx)(`span`,{className:`badge brand`,children:m(n)}),(0,N.jsx)(`span`,{className:`badge ${a.tone}`,children:a.label}),(0,N.jsx)(`span`,{className:`badge ${D.ok?`green`:`orange`}`,children:`gpt-image-2`}),(0,N.jsx)(`span`,{className:`badge ${O?.exported?`orange`:`muted`}`,children:k})]})]}),(0,N.jsxs)(`label`,{className:`account-select`,children:[(0,N.jsx)(`input`,{type:`checkbox`,checked:!!e.selectedProfiles[n.profileId],onChange:t=>e.onSelect(n.profileId,t.target.checked)}),(0,N.jsx)(`span`,{children:`选择`})]})]}),(0,N.jsxs)(`div`,{className:`account-metrics`,children:[(0,N.jsx)(L,{label:s(n,`primary`),value:o,tone:f(o)}),(0,N.jsx)(L,{label:s(n,`secondary`),value:p,tone:f(p)})]}),(0,N.jsxs)(`div`,{className:`usage-status-row`,children:[(0,N.jsxs)(`span`,{className:`usage-status ${n.isActive?`is-active`:``}`,children:[(0,N.jsx)(r,{size:14}),(0,N.jsx)(`span`,{children:`API`}),(0,N.jsx)(`span`,{className:`usage-dot ${n.isActive?`active`:``}`}),(0,N.jsx)(`span`,{className:`usage-state-text`,children:n.isActive?`使用中`:`未使用`})]}),(0,N.jsxs)(`span`,{className:`usage-status ${b?`is-active`:``}`,children:[(0,N.jsx)(A,{size:14}),(0,N.jsx)(`span`,{children:`Codex`}),(0,N.jsx)(`span`,{className:`usage-dot ${b?`active`:``}`}),(0,N.jsx)(`span`,{className:`usage-state-text`,children:b?`使用中`:`未使用`})]})]}),(0,N.jsxs)(`div`,{className:`compact-meta-row`,children:[(0,N.jsxs)(`div`,{className:`compact-reset-list`,children:[(0,N.jsxs)(`div`,{className:`compact-meta-item`,children:[(0,N.jsx)(`label`,{children:s(n,`primary`)}),(0,N.jsx)(`strong`,{children:C(n,`primary`)})]}),(0,N.jsxs)(`div`,{className:`compact-meta-item`,children:[(0,N.jsx)(`label`,{children:s(n,`secondary`)}),(0,N.jsx)(`strong`,{children:C(n,`secondary`)})]})]}),(0,N.jsx)(`div`,{className:`compact-meta-actions`,children:(0,N.jsxs)(`button`,{className:`details-toggle ${y?`is-expanded`:``}`,type:`button`,onClick:()=>e.onToggle(n.profileId),children:[(0,N.jsx)(`span`,{children:y?`收起详情`:`查看详情`}),(0,N.jsx)(I,{})]})})]}),y&&(0,N.jsxs)(`div`,{className:`meta-grid`,children:[(0,N.jsx)(j,{label:`套餐`,value:m(n)}),(0,N.jsx)(j,{label:`Account ID`,value:(e.showEmails,n.accountId),code:!0}),(0,N.jsx)(j,{label:`Profile ID`,value:(e.showEmails,n.profileId),code:!0}),(0,N.jsx)(j,{label:`认证状态`,value:x(n)}),(0,N.jsx)(j,{label:`生图能力`,value:D.ok?`gpt-image-2 可用`:D.detail}),(0,N.jsx)(j,{label:`导出记录`,value:F(O)}),(0,N.jsx)(j,{label:`过期时间`,value:n.expiresAt?new Date(n.expiresAt).toLocaleString(`zh-CN`):`-`}),(0,N.jsx)(j,{label:`额度快照`,value:n.quota?.capturedAt?new Date(n.quota.capturedAt).toLocaleString(`zh-CN`):`-`})]}),(0,N.jsxs)(`div`,{className:`account-actions`,children:[(0,N.jsx)(`button`,{className:`btn-secondary ${n.isActive?`is-current`:``}`,type:`button`,onClick:()=>e.onAction(`activate`,n),disabled:n.isActive||M||w,children:w?`网关不可用`:n.isActive?`网关使用中`:`应用网关`}),(0,N.jsx)(`button`,{className:`btn-secondary ${b?`is-current codex`:``}`,type:`button`,onClick:()=>e.onAction(`apply-codex`,n),disabled:b||M||w,children:w?`Codex 不可用`:b?`Codex 使用中`:`应用 Codex`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>e.onAction(`export`,n),disabled:M,children:`导出`}),(0,N.jsx)(`button`,{className:`btn-danger`,type:`button`,onClick:()=>e.onAction(`remove`,n),disabled:M,children:`删除`})]})]},n.profileId)})})]})}function F(e){if(!e?.exported)return`未导出`;let t=e.lastExportKind===`single`?`单账号导出`:e.lastExportKind===`batch`?`批量导出`:`全部导出`;return`${e.count} 次,最近 ${o(e.lastExportedAt)},方式 ${t}`}function I(){return(0,N.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,"aria-hidden":`true`,children:(0,N.jsx)(`path`,{d:`m6 9 6 6 6-6`})})}function L(e){return(0,N.jsxs)(`div`,{className:`quota-row`,children:[(0,N.jsxs)(`div`,{className:`quota-line`,children:[(0,N.jsxs)(`span`,{children:[e.label,` · 已用 `,e.value,`% / 剩余 `,100-e.value,`%`]}),(0,N.jsxs)(`strong`,{children:[`剩余 `,100-e.value,`%`]})]}),(0,N.jsx)(`div`,{className:`progress-track`,children:(0,N.jsx)(`div`,{className:`progress-bar ${e.tone}`,style:{width:`${e.value}%`}})})]})}function R(e){let[t,n]=(0,M.useState)({}),[r,i]=(0,M.useState)({}),[a,o]=(0,M.useState)({search:``,status:`all`,sort:`quota-desc`}),s=(0,M.useMemo)(()=>{let t=e.config?.profiles?[...e.config.profiles]:[],n=a.search.trim().toLowerCase(),r=t.filter(t=>{let r=[h(t,!0).toLowerCase(),t.accountId,t.profileId,t.email||``].join(` `).toLowerCase(),i=d(t),o=!!(e.codexAccountId&&t.accountId===e.codexAccountId);return n&&!r.includes(n)?!1:a.status===`active`?t.isActive||o:a.status===`healthy`?i.key===`healthy`:a.status===`warning`?i.key===`warning`:a.status===`exhausted`?i.key===`exhausted`:a.status===`expired`?i.key===`expired`:a.status===`invalid`?i.key===`invalid`:!0});return r.sort((t,n)=>{let r=p(t,e.codexAccountId)-p(n,e.codexAccountId);if(r!==0)return r;let i=y(n)-y(t);if(i!==0)return i;let o=S(n)-S(t);return o===0?a.sort===`latency-asc`?(n.quota?.capturedAt||0)-(t.quota?.capturedAt||0):a.sort===`expiry-asc`?(t.expiresAt||2**53-1)-(n.expiresAt||2**53-1):a.sort===`name-asc`?h(t,!0).localeCompare(h(n,!0),`zh-CN`):a.sort===`quota-asc`?100-u(n)-(100-u(t)):a.sort===`plan-desc`?y(n)-y(t):a.sort===`email-asc`?h(t,!0).localeCompare(h(n,!0)):u(n)-u(t):o}),r},[a,e.codexAccountId,e.config?.profiles]),c=Object.values(t).filter(Boolean).length,l=Object.keys(t).filter(e=>t[e]);async function f(t,n){let r=await O(`/_gateway/admin/profiles/export`,{method:`POST`,headers:{"Content-Type":`application/json`},body:w(n?{profileIds:n}:{profileId:t})});D(`ai-zero-token-${n?`profiles-${n.length}`:t||`active`}.json`,r.profile),r.config?e.setConfig(r.config):await e.refreshConfig({silent:!0}),e.setStatus(n?`已导出 ${n.length} 个账号。`:`账号配置已导出。`)}async function m(t,n){if(!(t===`remove`&&!window.confirm(`确认删除 ${h(n,e.showEmails)}?`))){if((t===`activate`||t===`apply-codex`)&&_(n)){e.setStatus(`${h(n,e.showEmails)} 登录已失效,不能应用到${t===`activate`?`网关`:`Codex`}。`);return}if((t===`activate`||t===`apply-codex`)&&b(n)){let r=t===`activate`?`网关`:`Codex`;if(!window.confirm(`${h(n,e.showEmails)} 的额度看起来已耗尽,仍要应用到${r}吗?`))return}if(t===`export`){await f(n.profileId);return}e.setBusy(`profile:${t}:${n.profileId}`);try{let r=await O({activate:`/_gateway/admin/profiles/activate`,"apply-codex":`/_gateway/admin/codex/apply`,"sync-quota":`/_gateway/admin/profiles/sync-quota`,remove:`/_gateway/admin/profiles/remove`}[t],{method:`POST`,headers:{"Content-Type":`application/json`},body:w({profileId:n.profileId})});e.setConfig(`config`in r?r.config:r),e.setStatus(t===`activate`?`已应用到网关。`:t===`apply-codex`?`已应用到本机 Codex。`:t===`sync-quota`?`额度信息已同步。`:`账号已删除。`)}catch(t){e.setStatus(k(t))}finally{e.setBusy(null)}}}async function g(){let t=l;if(t.length===0){e.setStatus(`请先勾选要删除的账号。`);return}let r=e.config?.profiles.filter(e=>t.includes(e.profileId)).slice(0,3).map(t=>h(t,e.showEmails)),i=r?.length?`\n\n${r.join(`
1
+ import{a as e,r as t,t as n}from"./jsx-runtime-DqpGtLhh.js";import{t as r}from"./earth-DFdZaQIi.js";import{t as i}from"./refresh-cw-CAAH2rqe.js";import{t as a}from"./search-B2hz41D3.js";import{C as o,_ as s,a as c,b as l,d as u,f as d,g as f,h as p,i as m,m as h,n as g,o as _,p as v,r as y,s as b,t as x,u as S,v as C,w,y as T}from"./profiles-DMOjJORP.js";import{_ as E,d as D,p as O,r as k,x as A}from"./index--rNjdmzf.js";import{t as j}from"./InfoRow-0ULI9iI3.js";var M=e(t(),1),N=n();function P(e){let t=e.config?.codex?.accountId,n=e.profiles.length<=0?``:e.profiles.length===1?`profile-count-1`:e.profiles.length===2?`profile-count-2`:e.profiles.length===3?`profile-count-3`:`profile-count-many`;return(0,N.jsxs)(`section`,{className:`card`,id:`accounts`,children:[(0,N.jsxs)(`div`,{className:`section-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`h2`,{children:`账号额度预览`}),(0,N.jsx)(`p`,{children:`账号信息采用卡片式布局展示,支持搜索、状态筛选和额度排序。`})]}),(0,N.jsxs)(`div`,{className:`section-actions`,children:[(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onLocate,children:`定位当前账号`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onExportSelected,children:`导出所选`}),(0,N.jsx)(`button`,{className:`btn-danger`,type:`button`,onClick:e.onRemoveSelected,disabled:e.selectedCount===0||e.busy===`bulk-remove`,children:`删除所选`}),(0,N.jsx)(`button`,{className:`btn-primary`,type:`button`,onClick:e.onAddAccount,children:`新增账号`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onRefreshStatus,children:`刷新状态`}),(0,N.jsx)(`button`,{className:`btn-danger`,type:`button`,onClick:e.onClearAccounts,children:`清空账号`})]})]}),(0,N.jsxs)(`div`,{className:`filter-row`,children:[(0,N.jsxs)(`label`,{className:`search-box`,children:[(0,N.jsx)(a,{size:16}),(0,N.jsx)(`input`,{value:e.filter.search,onChange:t=>e.onFilter({...e.filter,search:t.target.value}),placeholder:`搜索邮箱、账号 ID 或 Profile ID`})]}),(0,N.jsxs)(`select`,{className:`control`,value:e.filter.status,onChange:t=>e.onFilter({...e.filter,status:t.target.value}),children:[(0,N.jsx)(`option`,{value:`all`,children:`全部状态`}),(0,N.jsx)(`option`,{value:`active`,children:`使用中`}),(0,N.jsx)(`option`,{value:`healthy`,children:`健康`}),(0,N.jsx)(`option`,{value:`warning`,children:`即将耗尽`}),(0,N.jsx)(`option`,{value:`exhausted`,children:`额度耗尽`}),(0,N.jsx)(`option`,{value:`invalid`,children:`登录失效`}),(0,N.jsx)(`option`,{value:`expired`,children:`已过期`})]}),(0,N.jsxs)(`select`,{className:`control`,value:e.filter.sort,onChange:t=>e.onFilter({...e.filter,sort:t.target.value}),children:[(0,N.jsx)(`option`,{value:`quota-desc`,children:`默认排序`}),(0,N.jsx)(`option`,{value:`latency-asc`,children:`按额度更新时间`}),(0,N.jsx)(`option`,{value:`expiry-asc`,children:`按过期时间`}),(0,N.jsx)(`option`,{value:`name-asc`,children:`按名称排序`}),(0,N.jsx)(`option`,{value:`quota-asc`,children:`按剩余额度升序`}),(0,N.jsx)(`option`,{value:`plan-desc`,children:`按套餐排序`}),(0,N.jsx)(`option`,{value:`email-asc`,children:`按邮箱排序`})]}),(0,N.jsxs)(`span`,{className:`account-selected-count`,children:[`已选择 `,e.selectedCount,` 个`]})]}),(0,N.jsx)(`div`,{className:`account-grid ${n}`,children:e.profiles.length===0?(0,N.jsx)(`div`,{className:`empty-state`,children:`还没有匹配的账号。可以新增账号或调整筛选条件。`}):e.profiles.map(n=>{let a=d(n),o=u(n),p=T(n),y=!!e.expandedProfiles[n.profileId],b=!!(t&&n.accountId===t),S=l(n,b),w=_(n),D=c(n),O=n.exportAudit,k=O?.exported?`已导出 ${O.count} 次`:`未导出`,M=typeof e.busy==`string`&&e.busy.startsWith(`profile:`)&&e.busy.endsWith(n.profileId),P=e.busy===`profile:sync-quota:${n.profileId}`;return(0,N.jsxs)(`article`,{className:`account-card plan-${g(n)} ${w?`is-auth-invalid`:``}`,"data-profile-card":n.profileId,title:w?x(n):void 0,children:[S&&(0,N.jsx)(`span`,{className:`usage-corner ${S.className}`,children:(0,N.jsx)(`span`,{children:S.label})}),(0,N.jsxs)(`div`,{className:`account-head`,children:[(0,N.jsxs)(`div`,{className:`account-title`,children:[(0,N.jsxs)(`div`,{className:`account-name`,children:[(0,N.jsx)(`span`,{className:`avatar`,children:v(n)}),(0,N.jsx)(`strong`,{children:h(n,e.showEmails)}),(0,N.jsx)(`button`,{"aria-label":`刷新额度`,className:`account-icon-btn`,disabled:M,onClick:()=>e.onAction(`sync-quota`,n),title:`刷新额度`,type:`button`,children:P?(0,N.jsx)(E,{className:`spin`,size:14}):(0,N.jsx)(i,{size:14})})]}),(0,N.jsxs)(`div`,{className:`badge-row`,children:[(0,N.jsx)(`span`,{className:`badge brand`,children:m(n)}),(0,N.jsx)(`span`,{className:`badge ${a.tone}`,children:a.label}),(0,N.jsx)(`span`,{className:`badge ${D.ok?`green`:`orange`}`,children:`gpt-image-2`}),(0,N.jsx)(`span`,{className:`badge ${O?.exported?`orange`:`muted`}`,children:k})]})]}),(0,N.jsxs)(`label`,{className:`account-select`,children:[(0,N.jsx)(`input`,{type:`checkbox`,checked:!!e.selectedProfiles[n.profileId],onChange:t=>e.onSelect(n.profileId,t.target.checked)}),(0,N.jsx)(`span`,{children:`选择`})]})]}),(0,N.jsxs)(`div`,{className:`account-metrics`,children:[(0,N.jsx)(L,{label:s(n,`primary`),value:o,tone:f(o)}),(0,N.jsx)(L,{label:s(n,`secondary`),value:p,tone:f(p)})]}),(0,N.jsxs)(`div`,{className:`usage-status-row`,children:[(0,N.jsxs)(`span`,{className:`usage-status ${n.isActive?`is-active`:``}`,children:[(0,N.jsx)(r,{size:14}),(0,N.jsx)(`span`,{children:`API`}),(0,N.jsx)(`span`,{className:`usage-dot ${n.isActive?`active`:``}`}),(0,N.jsx)(`span`,{className:`usage-state-text`,children:n.isActive?`使用中`:`未使用`})]}),(0,N.jsxs)(`span`,{className:`usage-status ${b?`is-active`:``}`,children:[(0,N.jsx)(A,{size:14}),(0,N.jsx)(`span`,{children:`Codex`}),(0,N.jsx)(`span`,{className:`usage-dot ${b?`active`:``}`}),(0,N.jsx)(`span`,{className:`usage-state-text`,children:b?`使用中`:`未使用`})]})]}),(0,N.jsxs)(`div`,{className:`compact-meta-row`,children:[(0,N.jsxs)(`div`,{className:`compact-reset-list`,children:[(0,N.jsxs)(`div`,{className:`compact-meta-item`,children:[(0,N.jsx)(`label`,{children:s(n,`primary`)}),(0,N.jsx)(`strong`,{children:C(n,`primary`)})]}),(0,N.jsxs)(`div`,{className:`compact-meta-item`,children:[(0,N.jsx)(`label`,{children:s(n,`secondary`)}),(0,N.jsx)(`strong`,{children:C(n,`secondary`)})]})]}),(0,N.jsx)(`div`,{className:`compact-meta-actions`,children:(0,N.jsxs)(`button`,{className:`details-toggle ${y?`is-expanded`:``}`,type:`button`,onClick:()=>e.onToggle(n.profileId),children:[(0,N.jsx)(`span`,{children:y?`收起详情`:`查看详情`}),(0,N.jsx)(I,{})]})})]}),y&&(0,N.jsxs)(`div`,{className:`meta-grid`,children:[(0,N.jsx)(j,{label:`套餐`,value:m(n)}),(0,N.jsx)(j,{label:`Account ID`,value:(e.showEmails,n.accountId),code:!0}),(0,N.jsx)(j,{label:`Profile ID`,value:(e.showEmails,n.profileId),code:!0}),(0,N.jsx)(j,{label:`认证状态`,value:x(n)}),(0,N.jsx)(j,{label:`生图能力`,value:D.ok?`gpt-image-2 可用`:D.detail}),(0,N.jsx)(j,{label:`导出记录`,value:F(O)}),(0,N.jsx)(j,{label:`过期时间`,value:n.expiresAt?new Date(n.expiresAt).toLocaleString(`zh-CN`):`-`}),(0,N.jsx)(j,{label:`额度快照`,value:n.quota?.capturedAt?new Date(n.quota.capturedAt).toLocaleString(`zh-CN`):`-`})]}),(0,N.jsxs)(`div`,{className:`account-actions`,children:[(0,N.jsx)(`button`,{className:`btn-secondary ${n.isActive?`is-current`:``}`,type:`button`,onClick:()=>e.onAction(`activate`,n),disabled:n.isActive||M||w,children:w?`网关不可用`:n.isActive?`网关使用中`:`应用网关`}),(0,N.jsx)(`button`,{className:`btn-secondary ${b?`is-current codex`:``}`,type:`button`,onClick:()=>e.onAction(`apply-codex`,n),disabled:b||M||w,children:w?`Codex 不可用`:b?`Codex 使用中`:`应用 Codex`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>e.onAction(`export`,n),disabled:M,children:`导出`}),(0,N.jsx)(`button`,{className:`btn-danger`,type:`button`,onClick:()=>e.onAction(`remove`,n),disabled:M,children:`删除`})]})]},n.profileId)})})]})}function F(e){if(!e?.exported)return`未导出`;let t=e.lastExportKind===`single`?`单账号导出`:e.lastExportKind===`batch`?`批量导出`:`全部导出`;return`${e.count} 次,最近 ${o(e.lastExportedAt)},方式 ${t}`}function I(){return(0,N.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,"aria-hidden":`true`,children:(0,N.jsx)(`path`,{d:`m6 9 6 6 6-6`})})}function L(e){return(0,N.jsxs)(`div`,{className:`quota-row`,children:[(0,N.jsxs)(`div`,{className:`quota-line`,children:[(0,N.jsxs)(`span`,{children:[e.label,` · 已用 `,e.value,`% / 剩余 `,100-e.value,`%`]}),(0,N.jsxs)(`strong`,{children:[`剩余 `,100-e.value,`%`]})]}),(0,N.jsx)(`div`,{className:`progress-track`,children:(0,N.jsx)(`div`,{className:`progress-bar ${e.tone}`,style:{width:`${e.value}%`}})})]})}function R(e){let[t,n]=(0,M.useState)({}),[r,i]=(0,M.useState)({}),[a,o]=(0,M.useState)({search:``,status:`all`,sort:`quota-desc`}),s=(0,M.useMemo)(()=>{let t=e.config?.profiles?[...e.config.profiles]:[],n=a.search.trim().toLowerCase(),r=t.filter(t=>{let r=[h(t,!0).toLowerCase(),t.accountId,t.profileId,t.email||``].join(` `).toLowerCase(),i=d(t),o=!!(e.codexAccountId&&t.accountId===e.codexAccountId);return n&&!r.includes(n)?!1:a.status===`active`?t.isActive||o:a.status===`healthy`?i.key===`healthy`:a.status===`warning`?i.key===`warning`:a.status===`exhausted`?i.key===`exhausted`:a.status===`expired`?i.key===`expired`:a.status===`invalid`?i.key===`invalid`:!0});return r.sort((t,n)=>{let r=p(t,e.codexAccountId)-p(n,e.codexAccountId);if(r!==0)return r;let i=y(n)-y(t);if(i!==0)return i;let o=S(n)-S(t);return o===0?a.sort===`latency-asc`?(n.quota?.capturedAt||0)-(t.quota?.capturedAt||0):a.sort===`expiry-asc`?(t.expiresAt||2**53-1)-(n.expiresAt||2**53-1):a.sort===`name-asc`?h(t,!0).localeCompare(h(n,!0),`zh-CN`):a.sort===`quota-asc`?100-u(n)-(100-u(t)):a.sort===`plan-desc`?y(n)-y(t):a.sort===`email-asc`?h(t,!0).localeCompare(h(n,!0)):u(n)-u(t):o}),r},[a,e.codexAccountId,e.config?.profiles]),c=Object.values(t).filter(Boolean).length,l=Object.keys(t).filter(e=>t[e]);async function f(t,n){let r=await O(`/_gateway/admin/profiles/export`,{method:`POST`,headers:{"Content-Type":`application/json`},body:w(n?{profileIds:n}:{profileId:t})});D(`ai-zero-token-${n?`profiles-${n.length}`:t||`active`}.json`,r.profile),r.config?e.setConfig(r.config):await e.refreshConfig({silent:!0}),e.setStatus(n?`已导出 ${n.length} 个账号。`:`账号配置已导出。`)}async function m(t,n){if(!(t===`remove`&&!window.confirm(`确认删除 ${h(n,e.showEmails)}?`))){if((t===`activate`||t===`apply-codex`)&&_(n)){e.setStatus(`${h(n,e.showEmails)} 登录已失效,不能应用到${t===`activate`?`网关`:`Codex`}。`);return}if((t===`activate`||t===`apply-codex`)&&b(n)){let r=t===`activate`?`网关`:`Codex`;if(!window.confirm(`${h(n,e.showEmails)} 的额度看起来已耗尽,仍要应用到${r}吗?`))return}if(t===`export`){await f(n.profileId);return}e.setBusy(`profile:${t}:${n.profileId}`);try{let r=await O({activate:`/_gateway/admin/profiles/activate`,"apply-codex":`/_gateway/admin/codex/apply`,"sync-quota":`/_gateway/admin/profiles/sync-quota`,remove:`/_gateway/admin/profiles/remove`}[t],{method:`POST`,headers:{"Content-Type":`application/json`},body:w({profileId:n.profileId})}),i=`config`in r?r.config:r;if(e.setConfig(i),e.setStatus(t===`activate`?`已应用到网关。`:t===`apply-codex`?`已应用到本机 Codex。`:t===`sync-quota`?`额度信息已同步。`:`账号已删除。`),t===`apply-codex`)if(i.codexRestartSupported&&window.confirm(`Codex 账号已切换,是否现在重启 Codex 客户端?
2
+
3
+ Codex 通常在启动时读取本机 auth.json,重启后新账号会立即生效。`))try{await O(`/_gateway/admin/desktop/restart-codex`,{method:`POST`}),e.setStatus(`已应用到本机 Codex,并已重启 Codex 客户端。`)}catch(t){e.setStatus(`已应用到本机 Codex,但重启 Codex 失败: ${k(t)}`)}else e.setStatus(`已应用到本机 Codex,重启 Codex 客户端后生效。`)}catch(t){e.setStatus(k(t))}finally{e.setBusy(null)}}}async function g(){let t=l;if(t.length===0){e.setStatus(`请先勾选要删除的账号。`);return}let r=e.config?.profiles.filter(e=>t.includes(e.profileId)).slice(0,3).map(t=>h(t,e.showEmails)),i=r?.length?`\n\n${r.join(`
2
4
  `)}${t.length>r.length?`\n等 ${t.length} 个账号`:``}`:``;if(window.confirm(`确认删除所选 ${t.length} 个账号?此操作不可撤销。${i}`)){e.setBusy(`bulk-remove`),e.setStatus(`正在删除 ${t.length} 个账号...`);try{let r=await O(`/_gateway/admin/profiles/remove-batch`,{method:`POST`,headers:{"Content-Type":`application/json`},body:w({profileIds:t})});e.setConfig(r),n({}),e.setStatus(`已删除 ${r.removedProfileCount??t.length} 个账号。`)}catch(t){e.setStatus(`删除所选失败: ${k(t)}`)}finally{e.setBusy(null)}}}return(0,N.jsx)(P,{config:e.config,profiles:s,showEmails:e.showEmails,filter:a,selectedProfiles:t,expandedProfiles:r,selectedCount:c,busy:e.busy,onFilter:o,onSelect:(e,t)=>n(n=>({...n,[e]:t})),onToggle:e=>i(t=>({...t,[e]:!t[e]})),onAction:m,onLocate:()=>e.activeProfile&&document.querySelector(`[data-profile-card="${e.activeProfile.profileId}"]`)?.scrollIntoView({behavior:`smooth`,block:`center`}),onExportSelected:()=>{let t=l;if(t.length===0){e.setStatus(`请先勾选要导出的账号。`);return}f(void 0,t).catch(t=>e.setStatus(t instanceof Error?t.message:String(t)))},onRemoveSelected:()=>void g(),onAddAccount:()=>e.setAccountModalOpen(!0),onRefreshStatus:()=>e.refreshConfig({runtime:!0}),onClearAccounts:()=>e.logout()})}export{R as AccountsPage};
@@ -1,4 +1,4 @@
1
- import{a as e,n as t,r as n,t as r}from"./jsx-runtime-DqpGtLhh.js";import{t as i}from"./server-BrjJPb9D.js";import{b as a,f as o,g as s,n as c,v as l,y as u}from"./index-DRe-tByu.js";var d=t(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),f=`# AI-Zero-Token Local Gateway Skill
1
+ import{a as e,n as t,r as n,t as r}from"./jsx-runtime-DqpGtLhh.js";import{t as i}from"./server-BrjJPb9D.js";import{b as a,f as o,g as s,n as c,v as l,y as u}from"./index--rNjdmzf.js";var d=t(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),f=`# AI-Zero-Token Local Gateway Skill
2
2
 
3
3
  ## Purpose
4
4
 
@@ -1154,7 +1154,7 @@ Or:
1154
1154
  请根据这个 Skill,帮我做一个 AI 文案 + 生图工具。
1155
1155
  左侧输入需求,右侧显示 GPT-5.5 生成的文案和 GPT-Image-2 生成的图片。
1156
1156
  \`\`\`
1157
- `.match(/```/g)?.length||0)/2,headings:f.split(/\r?\n/).filter(e=>/^#{1,6}\s+/.test(e)).length}),[]),C=[{label:`用途`,value:`本地网关接入说明`},{label:`接入参数`,value:`baseURL + apiKey = ${v} / ${y}`},{label:`启动命令`,value:`azt start / npx ai-zero-token start`},{label:`常用接口`,value:`/v1/models · /v1/responses · /v1/chat/completions · /v1/images/generations`}],w=`baseURL = "${v}"\napiKey = "${y}"`,T=`import OpenAI from "openai";\n\nconst client = new OpenAI({\n apiKey: "${y}",\n baseURL: "${v}",\n});`,E=`curl ${v}/chat/completions \\\n -H "Content-Type: application/json" \\\n -d '{\n "model": "${e?.settings.defaultModel||`gpt-5.4`}",\n "messages": [{ "role": "user", "content": "Reply with OK only." }]\n }'`,D=`curl ${v}/responses \\\n -H "Content-Type: application/json" \\\n -d '{\n "model": "${e?.settings.defaultModel||`gpt-5.4`}",\n "input": "Reply with OK only."\n }'`;async function O(){r(await c(`# AI-Zero-Token Local Gateway Skill
1157
+ `.match(/```/g)?.length||0)/2,headings:f.split(/\r?\n/).filter(e=>/^#{1,6}\s+/.test(e)).length}),[]),C=[{label:`用途`,value:`本地网关接入说明`},{label:`接入参数`,value:`baseURL + apiKey = ${v} / ${y}`},{label:`启动命令`,value:`azt start / npx ai-zero-token start`},{label:`常用接口`,value:`/v1/models · /v1/responses · /v1/chat/completions · /v1/images/generations`}],w=`baseURL = "${v}"\napiKey = "${y}"`,T=`import OpenAI from "openai";\n\nconst client = new OpenAI({\n apiKey: "${y}",\n baseURL: "${v}",\n});`,E=`curl ${v}/chat/completions \\\n -H "Content-Type: application/json" \\\n -d '{\n "model": "${e?.settings.defaultModel||`gpt-5.4`}",\n "messages": [{ "role": "user", "content": "Reply with OK only." }]\n }'`,D=`Provider: OpenAI compatible\nBase URL: ${v}\nAPI Key: ${y}\nModel: ${e?.settings.defaultModel||`gpt-5.4`}\nChat endpoint: /chat/completions\nStreaming: enabled\nTools / function calling: enabled`,O=`curl ${v}/chat/completions \\\n -H "Content-Type: application/json" \\\n -d '{\n "model": "${e?.settings.defaultModel||`gpt-5.4`}",\n "stream": true,\n "messages": [{ "role": "user", "content": "Call the weather tool for Shanghai." }],\n "tools": [{\n "type": "function",\n "function": {\n "name": "get_weather",\n "description": "Get weather for a city.",\n "parameters": {\n "type": "object",\n "properties": { "city": { "type": "string" } },\n "required": ["city"]\n }\n }\n }],\n "tool_choice": "auto"\n }'`,k=`curl ${v}/responses \\\n -H "Content-Type: application/json" \\\n -d '{\n "model": "${e?.settings.defaultModel||`gpt-5.4`}",\n "input": "Reply with OK only."\n }'`;async function A(){r(await c(`# AI-Zero-Token Local Gateway Skill
1158
1158
 
1159
1159
  ## Purpose
1160
1160
 
@@ -1732,4 +1732,4 @@ Or:
1732
1732
  请根据这个 Skill,帮我做一个 AI 文案 + 生图工具。
1733
1733
  左侧输入需求,右侧显示 GPT-5.5 生成的文案和 GPT-Image-2 生成的图片。
1734
1734
  \`\`\`
1735
- `)?`Skill.md 已复制。`:`Skill.md 复制失败。`)}function k(){o(`AI-Zero-Token-Skill.md`,f,`text/markdown;charset=utf-8`),r(`Skill.md 已下载。`)}return(0,m.jsxs)(`section`,{className:`docs-page`,children:[(0,m.jsx)(`header`,{className:`docs-page-head docs-page-head-actions`,children:(0,m.jsxs)(`div`,{className:`docs-page-actions`,children:[(0,m.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>void O(),children:[(0,m.jsx)(a,{size:16}),`复制 Skill.md`]}),(0,m.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:k,children:[(0,m.jsx)(u,{size:16}),`下载 Skill.md`]}),(0,m.jsxs)(`button`,{className:`btn-primary`,type:`button`,onClick:()=>t(`tester`),children:[(0,m.jsx)(l,{size:16}),`打开接口测试`]})]})}),(0,m.jsxs)(`section`,{className:`docs-summary`,children:[(0,m.jsxs)(`div`,{className:`docs-summary-item`,children:[(0,m.jsx)(`span`,{children:`Base URL`}),(0,m.jsx)(`strong`,{children:v})]}),(0,m.jsxs)(`div`,{className:`docs-summary-item`,children:[(0,m.jsx)(`span`,{children:`API Key`}),(0,m.jsx)(`strong`,{children:y})]}),(0,m.jsxs)(`div`,{className:`docs-summary-item`,children:[(0,m.jsx)(`span`,{children:`启动命令`}),(0,m.jsx)(`strong`,{children:b})]}),(0,m.jsxs)(`div`,{className:`docs-summary-item`,children:[(0,m.jsx)(`span`,{children:`文档规模`}),(0,m.jsxs)(`strong`,{children:[S.lines,` 行 · `,S.headings,` 级标题 · `,S.codeBlocks,` 段代码`]})]})]}),(0,m.jsx)(`div`,{className:`docs-layout`,children:(0,m.jsxs)(`div`,{className:`docs-main`,children:[(0,m.jsxs)(`nav`,{className:`docs-tab-bar`,"aria-label":`Skill 文档视图切换`,children:[(0,m.jsx)(`button`,{className:g===`quick-start`?`is-active`:``,type:`button`,onClick:()=>_(`quick-start`),children:`快速接入`}),(0,m.jsx)(`button`,{className:g===`skill`?`is-active`:``,type:`button`,onClick:()=>_(`skill`),children:`Skill.md`}),(0,m.jsx)(`button`,{className:g===`examples`?`is-active`:``,type:`button`,onClick:()=>_(`examples`),children:`示例代码`})]}),g===`quick-start`?(0,m.jsxs)(`div`,{className:`docs-panel-grid`,children:[(0,m.jsxs)(`section`,{className:`docs-panel`,children:[(0,m.jsx)(`div`,{className:`docs-panel-head`,children:(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h3`,{children:`三步接入`}),(0,m.jsx)(`p`,{children:`先启动本地网关,再把 Skill.md 放进你的工具或项目。`})]})}),(0,m.jsxs)(`ol`,{className:`docs-step-list`,children:[(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`启动网关`}),(0,m.jsxs)(`span`,{children:[`执行 `,(0,m.jsx)(`code`,{children:b}),`,管理页默认在 `,(0,m.jsx)(`code`,{children:`http://127.0.0.1:8787`}),`。`]})]}),(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`复制接入参数`}),(0,m.jsxs)(`span`,{children:[`Base URL 用 `,(0,m.jsx)(`code`,{children:v}),`,API Key 用 `,(0,m.jsx)(`code`,{children:y}),`。`]})]}),(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`下载 Skill.md`}),(0,m.jsx)(`span`,{children:`把这份文档保存到你的工作流里,或直接复制给支持 Skill 的工具。`})]})]})]}),(0,m.jsxs)(`section`,{className:`docs-panel`,children:[(0,m.jsx)(`div`,{className:`docs-panel-head`,children:(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h3`,{children:`接入模板`}),(0,m.jsx)(`p`,{children:`最少只需要这两项。`})]})}),(0,m.jsxs)(`div`,{className:`docs-mini-grid`,children:[(0,m.jsxs)(`button`,{className:`docs-mini-copy`,type:`button`,onClick:n,children:[(0,m.jsx)(`span`,{children:`Base URL`}),(0,m.jsx)(`strong`,{children:v}),(0,m.jsx)(a,{size:14})]}),(0,m.jsxs)(`button`,{className:`docs-mini-copy`,type:`button`,onClick:()=>void c(y).then(e=>r(e?`API Key 已复制。`:`API Key 复制失败。`)),children:[(0,m.jsx)(`span`,{children:`API Key`}),(0,m.jsx)(`strong`,{children:y}),(0,m.jsx)(a,{size:14})]})]}),(0,m.jsx)(`pre`,{className:`docs-code-sample`,children:(0,m.jsx)(`code`,{children:w})})]}),(0,m.jsxs)(`section`,{className:`docs-panel docs-panel-wide`,children:[(0,m.jsx)(`div`,{className:`docs-panel-head`,children:(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h3`,{children:`常用接口`}),(0,m.jsx)(`p`,{children:`这个 Skill 文档覆盖了模型、对话和生图三类常见用法。`})]})}),(0,m.jsx)(`div`,{className:`docs-endpoint-grid`,children:[`/v1/models`,`/v1/chat/completions`,`/v1/responses`,`/v1/images/generations`].map(e=>(0,m.jsxs)(`div`,{className:`docs-endpoint`,children:[(0,m.jsx)(`span`,{children:e}),(0,m.jsx)(d,{size:14})]},e))})]})]}):null,g===`skill`?(0,m.jsxs)(`section`,{className:`docs-panel docs-preview-panel`,children:[(0,m.jsx)(`div`,{className:`docs-panel-head`,children:(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h3`,{children:`Skill.md 摘要`}),(0,m.jsx)(`p`,{children:`先看关键信息,再按需展开源码。`})]})}),(0,m.jsx)(`div`,{className:`docs-skill-summary`,children:C.map(e=>(0,m.jsxs)(`article`,{className:`docs-summary-tile`,children:[(0,m.jsx)(`span`,{children:e.label}),(0,m.jsx)(`strong`,{children:e.value})]},e.label))}),(0,m.jsxs)(`details`,{className:`docs-source-fold`,children:[(0,m.jsxs)(`summary`,{children:[`查看源码`,(0,m.jsxs)(`span`,{children:[x,` 行`]})]}),(0,m.jsx)(`pre`,{className:`docs-source`,children:(0,m.jsx)(`code`,{children:f})})]})]}):null,g===`examples`?(0,m.jsxs)(`div`,{className:`docs-example-grid`,children:[(0,m.jsx)(h,{title:`OpenAI SDK`,description:`适合前端和本地脚本。`,code:T,onCopy:()=>void c(T).then(e=>r(e?`OpenAI SDK 示例已复制。`:`复制失败。`))}),(0,m.jsx)(h,{title:`curl Chat Completions`,description:`最直接的接口自测方式。`,code:E,onCopy:()=>void c(E).then(e=>r(e?`Chat Completions 示例已复制。`:`复制失败。`))}),(0,m.jsx)(h,{title:`Responses API`,description:`适用于新式文本生成调用。`,code:D,onCopy:()=>void c(D).then(e=>r(e?`Responses API 示例已复制。`:`复制失败。`))}),(0,m.jsxs)(`section`,{className:`docs-panel docs-note-panel`,children:[(0,m.jsx)(`h3`,{children:`用户如何使用`}),(0,m.jsxs)(`ul`,{children:[(0,m.jsx)(`li`,{children:`在侧边栏打开“使用文档”。`}),(0,m.jsx)(`li`,{children:`先复制 Base URL 和 API Key,再下载 Skill.md。`}),(0,m.jsx)(`li`,{children:`把 Skill.md 放到你的 AI 工具、项目文档或自动化流程里。`}),(0,m.jsx)(`li`,{children:`需要验证时,直接跳到“接口测试”页面跑一条请求。`})]}),(0,m.jsxs)(`div`,{className:`docs-action-row`,children:[(0,m.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>t(`overview`),children:[(0,m.jsx)(i,{size:16}),`回到概览`]}),(0,m.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>t(`tester`),children:[(0,m.jsx)(s,{size:16}),`去接口测试`]})]})]})]}):null]})})]})}export{g as DocsPage};
1735
+ `)?`Skill.md 已复制。`:`Skill.md 复制失败。`)}function j(){o(`AI-Zero-Token-Skill.md`,f,`text/markdown;charset=utf-8`),r(`Skill.md 已下载。`)}return(0,m.jsxs)(`section`,{className:`docs-page`,children:[(0,m.jsx)(`header`,{className:`docs-page-head docs-page-head-actions`,children:(0,m.jsxs)(`div`,{className:`docs-page-actions`,children:[(0,m.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>void A(),children:[(0,m.jsx)(a,{size:16}),`复制 Skill.md`]}),(0,m.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:j,children:[(0,m.jsx)(u,{size:16}),`下载 Skill.md`]}),(0,m.jsxs)(`button`,{className:`btn-primary`,type:`button`,onClick:()=>t(`tester`),children:[(0,m.jsx)(l,{size:16}),`打开接口测试`]})]})}),(0,m.jsxs)(`section`,{className:`docs-summary`,children:[(0,m.jsxs)(`div`,{className:`docs-summary-item`,children:[(0,m.jsx)(`span`,{children:`Base URL`}),(0,m.jsx)(`strong`,{children:v})]}),(0,m.jsxs)(`div`,{className:`docs-summary-item`,children:[(0,m.jsx)(`span`,{children:`API Key`}),(0,m.jsx)(`strong`,{children:y})]}),(0,m.jsxs)(`div`,{className:`docs-summary-item`,children:[(0,m.jsx)(`span`,{children:`启动命令`}),(0,m.jsx)(`strong`,{children:b})]}),(0,m.jsxs)(`div`,{className:`docs-summary-item`,children:[(0,m.jsx)(`span`,{children:`文档规模`}),(0,m.jsxs)(`strong`,{children:[S.lines,` 行 · `,S.headings,` 级标题 · `,S.codeBlocks,` 段代码`]})]})]}),(0,m.jsx)(`div`,{className:`docs-layout`,children:(0,m.jsxs)(`div`,{className:`docs-main`,children:[(0,m.jsxs)(`nav`,{className:`docs-tab-bar`,"aria-label":`Skill 文档视图切换`,children:[(0,m.jsx)(`button`,{className:g===`quick-start`?`is-active`:``,type:`button`,onClick:()=>_(`quick-start`),children:`快速接入`}),(0,m.jsx)(`button`,{className:g===`openclaw`?`is-active`:``,type:`button`,onClick:()=>_(`openclaw`),children:`OpenClaw`}),(0,m.jsx)(`button`,{className:g===`skill`?`is-active`:``,type:`button`,onClick:()=>_(`skill`),children:`Skill.md`}),(0,m.jsx)(`button`,{className:g===`examples`?`is-active`:``,type:`button`,onClick:()=>_(`examples`),children:`示例代码`})]}),g===`quick-start`?(0,m.jsxs)(`div`,{className:`docs-panel-grid`,children:[(0,m.jsxs)(`section`,{className:`docs-panel`,children:[(0,m.jsx)(`div`,{className:`docs-panel-head`,children:(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h3`,{children:`三步接入`}),(0,m.jsx)(`p`,{children:`先启动本地网关,再把 Skill.md 放进你的工具或项目。`})]})}),(0,m.jsxs)(`ol`,{className:`docs-step-list`,children:[(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`启动网关`}),(0,m.jsxs)(`span`,{children:[`执行 `,(0,m.jsx)(`code`,{children:b}),`,管理页默认在 `,(0,m.jsx)(`code`,{children:`http://127.0.0.1:8787`}),`。`]})]}),(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`复制接入参数`}),(0,m.jsxs)(`span`,{children:[`Base URL 用 `,(0,m.jsx)(`code`,{children:v}),`,API Key 用 `,(0,m.jsx)(`code`,{children:y}),`。`]})]}),(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`strong`,{children:`下载 Skill.md`}),(0,m.jsx)(`span`,{children:`把这份文档保存到你的工作流里,或直接复制给支持 Skill 的工具。`})]})]})]}),(0,m.jsxs)(`section`,{className:`docs-panel`,children:[(0,m.jsx)(`div`,{className:`docs-panel-head`,children:(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h3`,{children:`接入模板`}),(0,m.jsx)(`p`,{children:`最少只需要这两项。`})]})}),(0,m.jsxs)(`div`,{className:`docs-mini-grid`,children:[(0,m.jsxs)(`button`,{className:`docs-mini-copy`,type:`button`,onClick:n,children:[(0,m.jsx)(`span`,{children:`Base URL`}),(0,m.jsx)(`strong`,{children:v}),(0,m.jsx)(a,{size:14})]}),(0,m.jsxs)(`button`,{className:`docs-mini-copy`,type:`button`,onClick:()=>void c(y).then(e=>r(e?`API Key 已复制。`:`API Key 复制失败。`)),children:[(0,m.jsx)(`span`,{children:`API Key`}),(0,m.jsx)(`strong`,{children:y}),(0,m.jsx)(a,{size:14})]})]}),(0,m.jsx)(`pre`,{className:`docs-code-sample`,children:(0,m.jsx)(`code`,{children:w})})]}),(0,m.jsxs)(`section`,{className:`docs-panel docs-panel-wide`,children:[(0,m.jsx)(`div`,{className:`docs-panel-head`,children:(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h3`,{children:`常用接口`}),(0,m.jsx)(`p`,{children:`这个 Skill 文档覆盖了模型、对话和生图三类常见用法。`})]})}),(0,m.jsx)(`div`,{className:`docs-endpoint-grid`,children:[`/v1/models`,`/v1/chat/completions`,`/v1/responses`,`/v1/images/generations`].map(e=>(0,m.jsxs)(`div`,{className:`docs-endpoint`,children:[(0,m.jsx)(`span`,{children:e}),(0,m.jsx)(d,{size:14})]},e))})]})]}):null,g===`openclaw`?(0,m.jsxs)(`div`,{className:`docs-example-grid`,children:[(0,m.jsx)(h,{title:`OpenClaw 接入参数`,description:`在 OpenClaw 里选择 OpenAI-compatible provider。`,code:D,onCopy:()=>void c(D).then(e=>r(e?`OpenClaw 接入参数已复制。`:`复制失败。`))}),(0,m.jsx)(h,{title:`流式工具调用自测`,description:`验证 stream=true、tools 和 tool_choice 是否能被客户端识别。`,code:O,onCopy:()=>void c(O).then(e=>r(e?`OpenClaw 工具调用示例已复制。`:`复制失败。`))}),(0,m.jsxs)(`section`,{className:`docs-panel docs-note-panel`,children:[(0,m.jsx)(`h3`,{children:`兼容范围`}),(0,m.jsxs)(`ul`,{children:[(0,m.jsxs)(`li`,{children:[`支持 `,(0,m.jsx)(`code`,{children:`stream=true`}),`,返回 OpenAI 风格 SSE chunk。`]}),(0,m.jsxs)(`li`,{children:[`支持 `,(0,m.jsx)(`code`,{children:`tools`}),`、`,(0,m.jsx)(`code`,{children:`tool_choice`}),`、`,(0,m.jsx)(`code`,{children:`parallel_tool_calls`}),` 和 `,(0,m.jsx)(`code`,{children:`reasoning_effort`}),`。`]}),(0,m.jsxs)(`li`,{children:[`支持 assistant `,(0,m.jsx)(`code`,{children:`tool_calls`}),` 和 tool role 结果消息回传。`]}),(0,m.jsx)(`li`,{children:`请求日志会识别 OpenClaw user agent,并展示安全摘要。`}),(0,m.jsxs)(`li`,{children:[(0,m.jsx)(`code`,{children:`/v1/responses`}),` 暂不支持流式,`,(0,m.jsx)(`code`,{children:`n > 1`}),` 暂不支持。`]})]})]})]}):null,g===`skill`?(0,m.jsxs)(`section`,{className:`docs-panel docs-preview-panel`,children:[(0,m.jsx)(`div`,{className:`docs-panel-head`,children:(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h3`,{children:`Skill.md 摘要`}),(0,m.jsx)(`p`,{children:`先看关键信息,再按需展开源码。`})]})}),(0,m.jsx)(`div`,{className:`docs-skill-summary`,children:C.map(e=>(0,m.jsxs)(`article`,{className:`docs-summary-tile`,children:[(0,m.jsx)(`span`,{children:e.label}),(0,m.jsx)(`strong`,{children:e.value})]},e.label))}),(0,m.jsxs)(`details`,{className:`docs-source-fold`,children:[(0,m.jsxs)(`summary`,{children:[`查看源码`,(0,m.jsxs)(`span`,{children:[x,` 行`]})]}),(0,m.jsx)(`pre`,{className:`docs-source`,children:(0,m.jsx)(`code`,{children:f})})]})]}):null,g===`examples`?(0,m.jsxs)(`div`,{className:`docs-example-grid`,children:[(0,m.jsx)(h,{title:`OpenAI SDK`,description:`适合前端和本地脚本。`,code:T,onCopy:()=>void c(T).then(e=>r(e?`OpenAI SDK 示例已复制。`:`复制失败。`))}),(0,m.jsx)(h,{title:`curl Chat Completions`,description:`最直接的接口自测方式。`,code:E,onCopy:()=>void c(E).then(e=>r(e?`Chat Completions 示例已复制。`:`复制失败。`))}),(0,m.jsx)(h,{title:`Responses API`,description:`适用于新式文本生成调用。`,code:k,onCopy:()=>void c(k).then(e=>r(e?`Responses API 示例已复制。`:`复制失败。`))}),(0,m.jsxs)(`section`,{className:`docs-panel docs-note-panel`,children:[(0,m.jsx)(`h3`,{children:`用户如何使用`}),(0,m.jsxs)(`ul`,{children:[(0,m.jsx)(`li`,{children:`在侧边栏打开“使用文档”。`}),(0,m.jsx)(`li`,{children:`先复制 Base URL 和 API Key,再下载 Skill.md。`}),(0,m.jsx)(`li`,{children:`把 Skill.md 放到你的 AI 工具、项目文档或自动化流程里。`}),(0,m.jsx)(`li`,{children:`需要验证时,直接跳到“接口测试”页面跑一条请求。`})]}),(0,m.jsxs)(`div`,{className:`docs-action-row`,children:[(0,m.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>t(`overview`),children:[(0,m.jsx)(i,{size:16}),`回到概览`]}),(0,m.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>t(`tester`),children:[(0,m.jsx)(s,{size:16}),`去接口测试`]})]})]})]}):null]})})]})}export{g as DocsPage};
@@ -1 +1 @@
1
- import{a as e,n as t,r as n,t as r}from"./jsx-runtime-DqpGtLhh.js";import{t as i}from"./circle-check-ZYtn9GqY.js";import{t as a}from"./upload-CwXb7Q1b.js";import{C as o,S as s}from"./profiles-DMOjJORP.js";import{_ as c,b as l,n as u,o as d,p as f,r as p}from"./index-DRe-tByu.js";var m=t(`chevron-down`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),h=t(`link-2`,[[`path`,{d:`M9 17H7A5 5 0 0 1 7 7h2`,key:`8i5ue5`}],[`path`,{d:`M15 7h2a5 5 0 1 1 0 10h-2`,key:`1b9ql8`}],[`line`,{x1:`8`,x2:`16`,y1:`12`,y2:`12`,key:`1jonct`}]]),g=t(`trash-2`,[[`path`,{d:`M10 11v6`,key:`nco0om`}],[`path`,{d:`M14 11v6`,key:`outv1u`}],[`path`,{d:`M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6`,key:`miytrc`}],[`path`,{d:`M3 6h18`,key:`d0wm0j`}],[`path`,{d:`M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2`,key:`e791ji`}]]),_=e(n(),1),v=r();function y(e,t){return e?`${e.owner}/${e.repository} · ${e.branch}`:t?.hasToken?`已保存,待验证`:`尚未保存 token`}function b(e,t){if(!e)return t;try{let t=JSON.parse(e);if(typeof t.error?.message==`string`&&t.error.message.trim())return t.error.message}catch{}return e||t}async function x(e,t){let n=await d(e);return new Promise((r,i)=>{let a=new XMLHttpRequest;a.open(`POST`,`/_gateway/image-bed/upload`),a.setRequestHeader(`Content-Type`,`application/json`),a.responseType=`text`,a.upload.onprogress=e=>{e.lengthComputable&&e.total>0&&t(e.loaded/e.total*100)},a.onerror=()=>i(Error(`上传请求失败。`)),a.onload=()=>{let e=a.responseText||``;if(a.status<200||a.status>=300){i(Error(b(e,`HTTP ${a.status}`)));return}try{r(JSON.parse(e))}catch{i(Error(`上传响应解析失败。`))}},a.send(JSON.stringify({filename:e.name,dataUrl:n}))})}function S(e){let[t,n]=(0,_.useState)(null),[r,d]=(0,_.useState)(``),[b,S]=(0,_.useState)(!0),[C,w]=(0,_.useState)(null),[T,E]=(0,_.useState)([]),[D,O]=(0,_.useState)(12),[k,A]=(0,_.useState)(`正在读取图床配置...`),[j,M]=(0,_.useState)(!1),[N,P]=(0,_.useState)(null),[F,I]=(0,_.useState)(null),L=(0,_.useRef)(null),R=(0,_.useMemo)(()=>y(C,t),[C,t]),z=(0,_.useMemo)(()=>T.slice(0,D),[T,D]);(0,_.useEffect)(()=>{let e=!0;async function t(){try{let t=await f(`/_gateway/image-bed/config`);if(!e)return;n(t),S(!t.hasToken),A(t.hasToken?`GitHub token 已保存,正在验证连接...`:`请先保存一个 GitHub token。`),t.hasToken&&await V(!1)}catch(t){if(!e)return;A(p(t))}try{let t=await f(`/_gateway/image-bed/history?limit=100`);if(!e)return;E(t.items)}catch(t){if(!e)return;A(e=>`${e} ${p(t)}`)}}return t(),()=>{e=!1}},[]);async function B(){E((await f(`/_gateway/image-bed/history?limit=100`)).items)}async function V(t=!0){t&&e.setBusy(`image-bed-save`);try{let t=await f(`/_gateway/image-bed/validate`,{method:`POST`});return w(t),A(`已连接到 ${t.owner}/${t.repository},默认分支 ${t.branch}。`),e.setStatus(`图床连接正常:${t.owner}/${t.repository}`),t}catch(t){let n=p(t);throw w(null),A(`连接失败: ${n}`),e.setStatus(n),t}finally{t&&e.setBusy(null)}}async function H(){let t=r.trim();if(!t){A(`请先填写 GitHub token。`);return}e.setBusy(`image-bed-save`);try{n(await f(`/_gateway/image-bed/config`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({token:t})})),d(``),S(!1),A(`GitHub token 已保存,正在验证...`),e.setStatus(`GitHub token 已保存。`);try{await V(!1),A(`GitHub token 已保存并验证。`)}catch(e){S(!0),A(`已保存,但验证失败: ${p(e)}`)}}catch(t){let n=p(t);A(`保存失败: ${n}`),e.setStatus(n)}finally{e.setBusy(null)}}async function U(){e.setBusy(`image-bed-save`);try{n(await f(`/_gateway/image-bed/config`,{method:`DELETE`})),w(null),d(``),S(!0),A(`GitHub token 已清除。`),e.setStatus(`GitHub token 已清除。`)}catch(t){let n=p(t);A(`清除失败: ${n}`),e.setStatus(n)}finally{e.setBusy(null)}}async function W(){S(!0),d(``)}async function G(e,t){if(!e.type.startsWith(`image/`))throw Error(`文件 ${e.name} 不是图片。`);return x(e,t)}async function K(n){let r=Array.from(n).filter(e=>e.type.startsWith(`image/`));if(r.length===0){A(`请选择图片文件。`);return}if(!t?.hasToken){A(`请先保存 GitHub token,再上传图片。`);return}e.setBusy(`image-bed-upload`);try{let t=[];for(let n=0;n<r.length;n+=1){let i=r[n];P({phase:`reading`,fileName:i.name,fileIndex:n+1,totalFiles:r.length,percent:n/r.length*100}),A(`正在读取 ${n+1}/${r.length}: ${i.name}`),e.setStatus(`正在读取 ${n+1}/${r.length}: ${i.name}`);let a=await G(i,e=>{P({phase:`uploading`,fileName:i.name,fileIndex:n+1,totalFiles:r.length,percent:(n+e/100)/r.length*100})}),o={id:a.path,createdAt:Date.now(),filename:a.filename,path:a.path,url:a.url,htmlUrl:a.htmlUrl,downloadUrl:a.downloadUrl,owner:a.owner,repository:a.repository,branch:a.branch,size:a.size,mimeType:a.mimeType,previewUrl:a.url,sha:a.sha};t.unshift(o),E(e=>[o,...e.filter(e=>e.id!==o.id)].slice(0,100))}O(e=>Math.max(e,Math.min(12,T.length+t.length))),A(`已上传 ${r.length} 张图片。`),e.setStatus(`已上传 ${r.length} 张图片。`),await B()}catch(t){let n=p(t);A(`上传失败: ${n}`),e.setStatus(n)}finally{P(null),e.setBusy(null)}}async function q(t){let n=await u(t)?`链接已复制。`:`链接复制失败。`;A(n),e.setStatus(n)}function J(e){let t=Array.from(e.currentTarget.files||[]);e.currentTarget.value=``,t.length!==0&&K(t)}async function Y(){await f(`/_gateway/image-bed/history`,{method:`DELETE`}),E([]),O(12),A(`历史记录已清空。`),e.setStatus(`历史记录已清空。`)}async function X(t){if(window.confirm(`确认从 GitHub 仓库删除 ${t.filename} 吗?删除后原链接会失效。`)){I(t.id),e.setBusy(`image-bed-delete`);try{E((await f(`/_gateway/image-bed/history/${encodeURIComponent(t.id)}`,{method:`DELETE`})).items),A(`已删除 ${t.filename}。`),e.setStatus(`已从图床删除 ${t.filename}。`)}catch(t){let n=p(t);A(`删除失败: ${n}`),e.setStatus(n)}finally{I(null),e.setBusy(null)}}}let Z=e.busy===`image-bed-save`||e.busy===`image-bed-upload`||e.busy===`image-bed-delete`,Q=T[0];return(0,v.jsxs)(`section`,{className:`image-bed-page`,children:[(0,v.jsxs)(`div`,{className:`image-bed-workbench`,children:[(0,v.jsxs)(`section`,{className:`image-bed-upload-panel ${j?`is-dragging`:``}`,children:[(0,v.jsxs)(`div`,{className:`image-bed-upload-copy`,children:[(0,v.jsx)(`span`,{children:t?.hasToken?`GitHub 图床已准备`:`先配置 GitHub token`}),(0,v.jsx)(`h2`,{children:`拖入图片,直接拿公网链接`}),(0,v.jsxs)(`p`,{children:[`文件会写入公开仓库 `,(0,v.jsx)(`strong`,{children:t?.repository||`azt-img-bed`}),` 的 `,(0,v.jsx)(`strong`,{children:t?.pathPrefix||`images`}),` 目录,上传结果会保存在本机历史里。`]})]}),(0,v.jsxs)(`div`,{className:`upload-dropzone ${j?`is-dragging`:``} ${t?.hasToken?``:`is-disabled`}`,role:`button`,tabIndex:0,onClick:()=>{if(!t?.hasToken){A(`请先保存 GitHub token,再上传图片。`);return}L.current&&(L.current.value=``,L.current.click())},onKeyDown:e=>{if(e.key===`Enter`||e.key===` `){if(e.preventDefault(),!t?.hasToken){A(`请先保存 GitHub token,再上传图片。`);return}L.current&&(L.current.value=``,L.current.click())}},onDragOver:e=>{e.preventDefault(),M(!0)},onDragLeave:()=>M(!1),onDrop:e=>{e.preventDefault(),M(!1),K(e.dataTransfer.files)},children:[(0,v.jsx)(`input`,{ref:L,className:`upload-dropzone-input`,type:`file`,accept:`image/*`,multiple:!0,onChange:J}),(0,v.jsx)(`div`,{className:`upload-dropzone-icon`,children:(0,v.jsx)(a,{size:22})}),(0,v.jsx)(`strong`,{children:`选择图片或拖拽到这里`}),(0,v.jsx)(`span`,{children:t?.hasToken?`支持批量上传,完成后自动生成可访问链接。`:`保存并验证 token 后即可上传。`})]}),N?(0,v.jsxs)(`div`,{className:`upload-progress-block`,"aria-live":`polite`,children:[(0,v.jsxs)(`div`,{className:`upload-progress-head`,children:[(0,v.jsx)(`strong`,{children:N.phase===`reading`?`正在读取`:`正在上传`}),(0,v.jsxs)(`span`,{children:[N.fileIndex,`/`,N.totalFiles,` · `,N.fileName]})]}),(0,v.jsx)(`div`,{className:`upload-progress-track`,role:`progressbar`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Math.round(N.percent),children:(0,v.jsx)(`div`,{className:`upload-progress-fill`,style:{width:`${Math.max(4,Math.min(100,N.percent))}%`}})})]}):Q?(0,v.jsxs)(`div`,{className:`image-bed-latest`,children:[(0,v.jsx)(`button`,{type:`button`,className:`image-bed-latest-preview`,onClick:()=>void q(Q.url),title:`点击复制链接`,children:(0,v.jsx)(`img`,{loading:`lazy`,decoding:`async`,src:Q.previewUrl,alt:Q.filename})}),(0,v.jsxs)(`div`,{className:`image-bed-latest-info`,children:[(0,v.jsx)(`span`,{children:`最近上传`}),(0,v.jsx)(`strong`,{children:Q.filename}),(0,v.jsx)(`code`,{children:Q.url})]}),(0,v.jsxs)(`button`,{className:`btn-primary`,type:`button`,onClick:()=>void q(Q.url),children:[(0,v.jsx)(l,{size:16}),`复制链接`]})]}):(0,v.jsx)(`div`,{className:`image-bed-upload-note`,children:k})]}),(0,v.jsxs)(`aside`,{className:`image-bed-side-stack`,children:[(0,v.jsxs)(`section`,{className:`image-bed-side-card`,children:[(0,v.jsxs)(`div`,{className:`image-bed-section-head`,children:[(0,v.jsx)(`h4`,{children:`连接`}),(0,v.jsx)(`span`,{className:`image-bed-status-dot ${C?`is-ok`:t?.hasToken?`is-warn`:``}`})]}),(0,v.jsx)(`strong`,{className:`image-bed-connection-label`,children:R}),(0,v.jsx)(`p`,{children:k})]}),(0,v.jsxs)(`section`,{className:`image-bed-side-card`,children:[(0,v.jsxs)(`div`,{className:`image-bed-section-head`,children:[(0,v.jsx)(`h4`,{children:`Token`}),!b&&t?.hasToken&&(0,v.jsx)(`button`,{className:`image-bed-link-button`,type:`button`,onClick:()=>void W(),children:`修改`})]}),b||!t?.hasToken?(0,v.jsxs)(v.Fragment,{children:[(0,v.jsxs)(`label`,{className:`field`,children:[(0,v.jsx)(`span`,{children:`GitHub token`}),(0,v.jsx)(`input`,{className:`input`,value:r,onChange:e=>d(e.target.value),placeholder:`github_pat_...`,spellCheck:!1,autoComplete:`off`}),(0,v.jsxs)(`p`,{className:`image-bed-token-hint`,children:[`推荐使用 fine-grained token,只给公开仓库 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),` 的 Contents 读写权限。`]})]}),(0,v.jsxs)(`div`,{className:`image-bed-token-actions`,children:[(0,v.jsxs)(`button`,{className:`btn-primary`,type:`button`,onClick:()=>void H(),disabled:Z||!r.trim(),children:[e.busy===`image-bed-save`?(0,v.jsx)(c,{className:`spin`,size:16}):(0,v.jsx)(i,{size:16}),`保存并验证`]}),(0,v.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:U,disabled:!t?.hasToken,children:[(0,v.jsx)(g,{size:16}),`清除`]})]})]}):(0,v.jsxs)(`div`,{className:`image-bed-token-summary`,children:[(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`span`,{children:`已保存到本机`}),(0,v.jsx)(`strong`,{children:`GitHub token`})]}),(0,v.jsx)(`button`,{className:`btn-secondary icon-only`,type:`button`,onClick:U,title:`清除 Token`,children:(0,v.jsx)(g,{size:16})})]})]}),(0,v.jsxs)(`section`,{className:`image-bed-side-card image-bed-target-card`,children:[(0,v.jsx)(`h4`,{children:`目标`}),(0,v.jsxs)(`div`,{className:`image-bed-target-list`,children:[(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`span`,{children:`仓库`}),(0,v.jsx)(`strong`,{children:t?.repository||`azt-img-bed`})]}),(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`span`,{children:`分支`}),(0,v.jsx)(`strong`,{children:t?.defaultBranch||`auto`})]}),(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`span`,{children:`目录`}),(0,v.jsx)(`strong`,{children:t?.pathPrefix||`images`})]})]})]})]})]}),(0,v.jsxs)(`section`,{className:`image-bed-gallery-section`,children:[(0,v.jsxs)(`div`,{className:`image-bed-section-head`,children:[(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`h4`,{children:`上传历史`}),(0,v.jsx)(`p`,{children:`本机保存最近 100 条,只加载当前可见缩略图。`})]}),(0,v.jsx)(`button`,{className:`btn-secondary icon-only`,type:`button`,onClick:()=>void Y(),disabled:T.length===0,title:`清空历史`,children:(0,v.jsx)(g,{size:16})})]}),T.length===0?(0,v.jsx)(`div`,{className:`image-bed-empty`,children:`还没有上传记录。上传完成后,这里会以图库形式展示预览和链接。`}):(0,v.jsx)(`div`,{className:`image-bed-results-grid`,children:z.map(e=>(0,v.jsxs)(`figure`,{className:`image-bed-result-card`,children:[(0,v.jsx)(`button`,{type:`button`,className:`image-bed-preview-button`,onClick:()=>void q(e.url),title:`点击复制链接`,children:(0,v.jsx)(`img`,{loading:`lazy`,decoding:`async`,src:e.previewUrl,alt:e.filename})}),(0,v.jsxs)(`figcaption`,{children:[(0,v.jsx)(`strong`,{children:e.filename}),(0,v.jsxs)(`span`,{children:[s(e.size),` · `,e.mimeType]}),(0,v.jsx)(`code`,{children:o(e.createdAt)})]}),(0,v.jsxs)(`div`,{className:`image-bed-result-actions`,children:[(0,v.jsx)(`button`,{className:`image-bed-card-action`,type:`button`,onClick:()=>void q(e.url),title:`复制链接`,"aria-label":`复制链接`,children:(0,v.jsx)(l,{size:15})}),(0,v.jsx)(`a`,{className:`image-bed-card-action`,href:e.url,target:`_blank`,rel:`noreferrer`,title:`打开原图`,"aria-label":`打开原图`,children:(0,v.jsx)(h,{size:15})}),(0,v.jsx)(`button`,{className:`image-bed-card-action is-danger`,type:`button`,onClick:()=>void X(e),title:`删除图床文件`,"aria-label":`删除图床文件`,disabled:F===e.id,children:F===e.id?(0,v.jsx)(c,{className:`spin`,size:15}):(0,v.jsx)(g,{size:15})})]})]},e.path))}),T.length>D&&(0,v.jsxs)(`button`,{className:`btn-secondary image-bed-load-more`,type:`button`,onClick:()=>O(e=>Math.min(e+12,T.length)),children:[(0,v.jsx)(m,{size:16}),`加载更多`]})]}),(0,v.jsxs)(`details`,{className:`image-bed-help-section`,children:[(0,v.jsx)(`summary`,{children:`GitHub token 创建说明`}),(0,v.jsxs)(`p`,{className:`image-bed-help-intro`,children:[`这个图床固定使用当前 GitHub 账号下的公开仓库 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),`,图片会写入 `,(0,v.jsx)(`code`,{children:`images`}),` 目录并返回 raw.githubusercontent.com 原图链接。`]}),(0,v.jsxs)(`ol`,{className:`image-bed-steps`,children:[(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`先建公开仓库。`}),` 在 GitHub 新建仓库 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),`,Visibility 选 `,(0,v.jsx)(`strong`,{children:`Public`}),`。如果仓库已经存在,确认它属于这个 token 对应的个人账号,并且不是 Private。`]}),(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`创建 fine-grained token。`}),` 打开 GitHub 的 `,(0,v.jsx)(`a`,{href:`https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens`,target:`_blank`,rel:`noreferrer`,children:`Personal access tokens`}),` 页面,选择 `,(0,v.jsx)(`strong`,{children:`Fine-grained tokens`}),`,点 `,(0,v.jsx)(`strong`,{children:`Generate new token`}),`。`]}),(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`限制仓库范围。`}),` Token name 可填 `,(0,v.jsx)(`code`,{children:`AI Zero Token image bed`}),`,Expiration 按需设置,Resource owner 选自己的账号;Repository access 选择 `,(0,v.jsx)(`strong`,{children:`Only select repositories`}),`,只勾选 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),`。`]}),(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`给最小权限。`}),` 在 Repository permissions 里把 `,(0,v.jsx)(`strong`,{children:`Contents`}),` 设置为 `,(0,v.jsx)(`strong`,{children:`Read and write`}),`;`,(0,v.jsx)(`strong`,{children:`Metadata`}),` 保持默认 Read-only 即可,其它权限不需要打开。`]}),(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`复制并验证。`}),` GitHub 只会展示一次生成后的 token,复制后回到这里粘贴,点击 `,(0,v.jsx)(`strong`,{children:`保存并验证`}),`。验证通过后就可以拖图上传。`]})]}),(0,v.jsxs)(`dl`,{className:`image-bed-help-facts`,children:[(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`dt`,{children:`Token 格式`}),(0,v.jsxs)(`dd`,{children:[`fine-grained token 通常以 `,(0,v.jsx)(`code`,{children:`github_pat_`}),` 开头。`]})]}),(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`dt`,{children:`验证失败`}),(0,v.jsxs)(`dd`,{children:[`如果提示未找到仓库,优先检查仓库名是否为 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),`、仓库是否 Public、Repository access 是否选中了这个仓库。`]})]})]}),(0,v.jsx)(`p`,{className:`hint`,children:`Token 要像密码一样保管,不要发给别人。这个页面只会把它保存到你本机状态目录。`})]})]})}export{S as ImageBedPage};
1
+ import{a as e,n as t,r as n,t as r}from"./jsx-runtime-DqpGtLhh.js";import{t as i}from"./circle-check-ZYtn9GqY.js";import{t as a}from"./upload-CwXb7Q1b.js";import{C as o,S as s}from"./profiles-DMOjJORP.js";import{_ as c,b as l,n as u,o as d,p as f,r as p}from"./index--rNjdmzf.js";var m=t(`chevron-down`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),h=t(`link-2`,[[`path`,{d:`M9 17H7A5 5 0 0 1 7 7h2`,key:`8i5ue5`}],[`path`,{d:`M15 7h2a5 5 0 1 1 0 10h-2`,key:`1b9ql8`}],[`line`,{x1:`8`,x2:`16`,y1:`12`,y2:`12`,key:`1jonct`}]]),g=t(`trash-2`,[[`path`,{d:`M10 11v6`,key:`nco0om`}],[`path`,{d:`M14 11v6`,key:`outv1u`}],[`path`,{d:`M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6`,key:`miytrc`}],[`path`,{d:`M3 6h18`,key:`d0wm0j`}],[`path`,{d:`M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2`,key:`e791ji`}]]),_=e(n(),1),v=r();function y(e,t){return e?`${e.owner}/${e.repository} · ${e.branch}`:t?.hasToken?`已保存,待验证`:`尚未保存 token`}function b(e,t){if(!e)return t;try{let t=JSON.parse(e);if(typeof t.error?.message==`string`&&t.error.message.trim())return t.error.message}catch{}return e||t}async function x(e,t){let n=await d(e);return new Promise((r,i)=>{let a=new XMLHttpRequest;a.open(`POST`,`/_gateway/image-bed/upload`),a.setRequestHeader(`Content-Type`,`application/json`),a.responseType=`text`,a.upload.onprogress=e=>{e.lengthComputable&&e.total>0&&t(e.loaded/e.total*100)},a.onerror=()=>i(Error(`上传请求失败。`)),a.onload=()=>{let e=a.responseText||``;if(a.status<200||a.status>=300){i(Error(b(e,`HTTP ${a.status}`)));return}try{r(JSON.parse(e))}catch{i(Error(`上传响应解析失败。`))}},a.send(JSON.stringify({filename:e.name,dataUrl:n}))})}function S(e){let[t,n]=(0,_.useState)(null),[r,d]=(0,_.useState)(``),[b,S]=(0,_.useState)(!0),[C,w]=(0,_.useState)(null),[T,E]=(0,_.useState)([]),[D,O]=(0,_.useState)(12),[k,A]=(0,_.useState)(`正在读取图床配置...`),[j,M]=(0,_.useState)(!1),[N,P]=(0,_.useState)(null),[F,I]=(0,_.useState)(null),L=(0,_.useRef)(null),R=(0,_.useMemo)(()=>y(C,t),[C,t]),z=(0,_.useMemo)(()=>T.slice(0,D),[T,D]);(0,_.useEffect)(()=>{let e=!0;async function t(){try{let t=await f(`/_gateway/image-bed/config`);if(!e)return;n(t),S(!t.hasToken),A(t.hasToken?`GitHub token 已保存,正在验证连接...`:`请先保存一个 GitHub token。`),t.hasToken&&await V(!1)}catch(t){if(!e)return;A(p(t))}try{let t=await f(`/_gateway/image-bed/history?limit=100`);if(!e)return;E(t.items)}catch(t){if(!e)return;A(e=>`${e} ${p(t)}`)}}return t(),()=>{e=!1}},[]);async function B(){E((await f(`/_gateway/image-bed/history?limit=100`)).items)}async function V(t=!0){t&&e.setBusy(`image-bed-save`);try{let t=await f(`/_gateway/image-bed/validate`,{method:`POST`});return w(t),A(`已连接到 ${t.owner}/${t.repository},默认分支 ${t.branch}。`),e.setStatus(`图床连接正常:${t.owner}/${t.repository}`),t}catch(t){let n=p(t);throw w(null),A(`连接失败: ${n}`),e.setStatus(n),t}finally{t&&e.setBusy(null)}}async function H(){let t=r.trim();if(!t){A(`请先填写 GitHub token。`);return}e.setBusy(`image-bed-save`);try{n(await f(`/_gateway/image-bed/config`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({token:t})})),d(``),S(!1),A(`GitHub token 已保存,正在验证...`),e.setStatus(`GitHub token 已保存。`);try{await V(!1),A(`GitHub token 已保存并验证。`)}catch(e){S(!0),A(`已保存,但验证失败: ${p(e)}`)}}catch(t){let n=p(t);A(`保存失败: ${n}`),e.setStatus(n)}finally{e.setBusy(null)}}async function U(){e.setBusy(`image-bed-save`);try{n(await f(`/_gateway/image-bed/config`,{method:`DELETE`})),w(null),d(``),S(!0),A(`GitHub token 已清除。`),e.setStatus(`GitHub token 已清除。`)}catch(t){let n=p(t);A(`清除失败: ${n}`),e.setStatus(n)}finally{e.setBusy(null)}}async function W(){S(!0),d(``)}async function G(e,t){if(!e.type.startsWith(`image/`))throw Error(`文件 ${e.name} 不是图片。`);return x(e,t)}async function K(n){let r=Array.from(n).filter(e=>e.type.startsWith(`image/`));if(r.length===0){A(`请选择图片文件。`);return}if(!t?.hasToken){A(`请先保存 GitHub token,再上传图片。`);return}e.setBusy(`image-bed-upload`);try{let t=[];for(let n=0;n<r.length;n+=1){let i=r[n];P({phase:`reading`,fileName:i.name,fileIndex:n+1,totalFiles:r.length,percent:n/r.length*100}),A(`正在读取 ${n+1}/${r.length}: ${i.name}`),e.setStatus(`正在读取 ${n+1}/${r.length}: ${i.name}`);let a=await G(i,e=>{P({phase:`uploading`,fileName:i.name,fileIndex:n+1,totalFiles:r.length,percent:(n+e/100)/r.length*100})}),o={id:a.path,createdAt:Date.now(),filename:a.filename,path:a.path,url:a.url,htmlUrl:a.htmlUrl,downloadUrl:a.downloadUrl,owner:a.owner,repository:a.repository,branch:a.branch,size:a.size,mimeType:a.mimeType,previewUrl:a.url,sha:a.sha};t.unshift(o),E(e=>[o,...e.filter(e=>e.id!==o.id)].slice(0,100))}O(e=>Math.max(e,Math.min(12,T.length+t.length))),A(`已上传 ${r.length} 张图片。`),e.setStatus(`已上传 ${r.length} 张图片。`),await B()}catch(t){let n=p(t);A(`上传失败: ${n}`),e.setStatus(n)}finally{P(null),e.setBusy(null)}}async function q(t){let n=await u(t)?`链接已复制。`:`链接复制失败。`;A(n),e.setStatus(n)}function J(e){let t=Array.from(e.currentTarget.files||[]);e.currentTarget.value=``,t.length!==0&&K(t)}async function Y(){await f(`/_gateway/image-bed/history`,{method:`DELETE`}),E([]),O(12),A(`历史记录已清空。`),e.setStatus(`历史记录已清空。`)}async function X(t){if(window.confirm(`确认从 GitHub 仓库删除 ${t.filename} 吗?删除后原链接会失效。`)){I(t.id),e.setBusy(`image-bed-delete`);try{E((await f(`/_gateway/image-bed/history/${encodeURIComponent(t.id)}`,{method:`DELETE`})).items),A(`已删除 ${t.filename}。`),e.setStatus(`已从图床删除 ${t.filename}。`)}catch(t){let n=p(t);A(`删除失败: ${n}`),e.setStatus(n)}finally{I(null),e.setBusy(null)}}}let Z=e.busy===`image-bed-save`||e.busy===`image-bed-upload`||e.busy===`image-bed-delete`,Q=T[0];return(0,v.jsxs)(`section`,{className:`image-bed-page`,children:[(0,v.jsxs)(`div`,{className:`image-bed-workbench`,children:[(0,v.jsxs)(`section`,{className:`image-bed-upload-panel ${j?`is-dragging`:``}`,children:[(0,v.jsxs)(`div`,{className:`image-bed-upload-copy`,children:[(0,v.jsx)(`span`,{children:t?.hasToken?`GitHub 图床已准备`:`先配置 GitHub token`}),(0,v.jsx)(`h2`,{children:`拖入图片,直接拿公网链接`}),(0,v.jsxs)(`p`,{children:[`文件会写入公开仓库 `,(0,v.jsx)(`strong`,{children:t?.repository||`azt-img-bed`}),` 的 `,(0,v.jsx)(`strong`,{children:t?.pathPrefix||`images`}),` 目录,上传结果会保存在本机历史里。`]})]}),(0,v.jsxs)(`div`,{className:`upload-dropzone ${j?`is-dragging`:``} ${t?.hasToken?``:`is-disabled`}`,role:`button`,tabIndex:0,onClick:()=>{if(!t?.hasToken){A(`请先保存 GitHub token,再上传图片。`);return}L.current&&(L.current.value=``,L.current.click())},onKeyDown:e=>{if(e.key===`Enter`||e.key===` `){if(e.preventDefault(),!t?.hasToken){A(`请先保存 GitHub token,再上传图片。`);return}L.current&&(L.current.value=``,L.current.click())}},onDragOver:e=>{e.preventDefault(),M(!0)},onDragLeave:()=>M(!1),onDrop:e=>{e.preventDefault(),M(!1),K(e.dataTransfer.files)},children:[(0,v.jsx)(`input`,{ref:L,className:`upload-dropzone-input`,type:`file`,accept:`image/*`,multiple:!0,onChange:J}),(0,v.jsx)(`div`,{className:`upload-dropzone-icon`,children:(0,v.jsx)(a,{size:22})}),(0,v.jsx)(`strong`,{children:`选择图片或拖拽到这里`}),(0,v.jsx)(`span`,{children:t?.hasToken?`支持批量上传,完成后自动生成可访问链接。`:`保存并验证 token 后即可上传。`})]}),N?(0,v.jsxs)(`div`,{className:`upload-progress-block`,"aria-live":`polite`,children:[(0,v.jsxs)(`div`,{className:`upload-progress-head`,children:[(0,v.jsx)(`strong`,{children:N.phase===`reading`?`正在读取`:`正在上传`}),(0,v.jsxs)(`span`,{children:[N.fileIndex,`/`,N.totalFiles,` · `,N.fileName]})]}),(0,v.jsx)(`div`,{className:`upload-progress-track`,role:`progressbar`,"aria-valuemin":0,"aria-valuemax":100,"aria-valuenow":Math.round(N.percent),children:(0,v.jsx)(`div`,{className:`upload-progress-fill`,style:{width:`${Math.max(4,Math.min(100,N.percent))}%`}})})]}):Q?(0,v.jsxs)(`div`,{className:`image-bed-latest`,children:[(0,v.jsx)(`button`,{type:`button`,className:`image-bed-latest-preview`,onClick:()=>void q(Q.url),title:`点击复制链接`,children:(0,v.jsx)(`img`,{loading:`lazy`,decoding:`async`,src:Q.previewUrl,alt:Q.filename})}),(0,v.jsxs)(`div`,{className:`image-bed-latest-info`,children:[(0,v.jsx)(`span`,{children:`最近上传`}),(0,v.jsx)(`strong`,{children:Q.filename}),(0,v.jsx)(`code`,{children:Q.url})]}),(0,v.jsxs)(`button`,{className:`btn-primary`,type:`button`,onClick:()=>void q(Q.url),children:[(0,v.jsx)(l,{size:16}),`复制链接`]})]}):(0,v.jsx)(`div`,{className:`image-bed-upload-note`,children:k})]}),(0,v.jsxs)(`aside`,{className:`image-bed-side-stack`,children:[(0,v.jsxs)(`section`,{className:`image-bed-side-card`,children:[(0,v.jsxs)(`div`,{className:`image-bed-section-head`,children:[(0,v.jsx)(`h4`,{children:`连接`}),(0,v.jsx)(`span`,{className:`image-bed-status-dot ${C?`is-ok`:t?.hasToken?`is-warn`:``}`})]}),(0,v.jsx)(`strong`,{className:`image-bed-connection-label`,children:R}),(0,v.jsx)(`p`,{children:k})]}),(0,v.jsxs)(`section`,{className:`image-bed-side-card`,children:[(0,v.jsxs)(`div`,{className:`image-bed-section-head`,children:[(0,v.jsx)(`h4`,{children:`Token`}),!b&&t?.hasToken&&(0,v.jsx)(`button`,{className:`image-bed-link-button`,type:`button`,onClick:()=>void W(),children:`修改`})]}),b||!t?.hasToken?(0,v.jsxs)(v.Fragment,{children:[(0,v.jsxs)(`label`,{className:`field`,children:[(0,v.jsx)(`span`,{children:`GitHub token`}),(0,v.jsx)(`input`,{className:`input`,value:r,onChange:e=>d(e.target.value),placeholder:`github_pat_...`,spellCheck:!1,autoComplete:`off`}),(0,v.jsxs)(`p`,{className:`image-bed-token-hint`,children:[`推荐使用 fine-grained token,只给公开仓库 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),` 的 Contents 读写权限。`]})]}),(0,v.jsxs)(`div`,{className:`image-bed-token-actions`,children:[(0,v.jsxs)(`button`,{className:`btn-primary`,type:`button`,onClick:()=>void H(),disabled:Z||!r.trim(),children:[e.busy===`image-bed-save`?(0,v.jsx)(c,{className:`spin`,size:16}):(0,v.jsx)(i,{size:16}),`保存并验证`]}),(0,v.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:U,disabled:!t?.hasToken,children:[(0,v.jsx)(g,{size:16}),`清除`]})]})]}):(0,v.jsxs)(`div`,{className:`image-bed-token-summary`,children:[(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`span`,{children:`已保存到本机`}),(0,v.jsx)(`strong`,{children:`GitHub token`})]}),(0,v.jsx)(`button`,{className:`btn-secondary icon-only`,type:`button`,onClick:U,title:`清除 Token`,children:(0,v.jsx)(g,{size:16})})]})]}),(0,v.jsxs)(`section`,{className:`image-bed-side-card image-bed-target-card`,children:[(0,v.jsx)(`h4`,{children:`目标`}),(0,v.jsxs)(`div`,{className:`image-bed-target-list`,children:[(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`span`,{children:`仓库`}),(0,v.jsx)(`strong`,{children:t?.repository||`azt-img-bed`})]}),(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`span`,{children:`分支`}),(0,v.jsx)(`strong`,{children:t?.defaultBranch||`auto`})]}),(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`span`,{children:`目录`}),(0,v.jsx)(`strong`,{children:t?.pathPrefix||`images`})]})]})]})]})]}),(0,v.jsxs)(`section`,{className:`image-bed-gallery-section`,children:[(0,v.jsxs)(`div`,{className:`image-bed-section-head`,children:[(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`h4`,{children:`上传历史`}),(0,v.jsx)(`p`,{children:`本机保存最近 100 条,只加载当前可见缩略图。`})]}),(0,v.jsx)(`button`,{className:`btn-secondary icon-only`,type:`button`,onClick:()=>void Y(),disabled:T.length===0,title:`清空历史`,children:(0,v.jsx)(g,{size:16})})]}),T.length===0?(0,v.jsx)(`div`,{className:`image-bed-empty`,children:`还没有上传记录。上传完成后,这里会以图库形式展示预览和链接。`}):(0,v.jsx)(`div`,{className:`image-bed-results-grid`,children:z.map(e=>(0,v.jsxs)(`figure`,{className:`image-bed-result-card`,children:[(0,v.jsx)(`button`,{type:`button`,className:`image-bed-preview-button`,onClick:()=>void q(e.url),title:`点击复制链接`,children:(0,v.jsx)(`img`,{loading:`lazy`,decoding:`async`,src:e.previewUrl,alt:e.filename})}),(0,v.jsxs)(`figcaption`,{children:[(0,v.jsx)(`strong`,{children:e.filename}),(0,v.jsxs)(`span`,{children:[s(e.size),` · `,e.mimeType]}),(0,v.jsx)(`code`,{children:o(e.createdAt)})]}),(0,v.jsxs)(`div`,{className:`image-bed-result-actions`,children:[(0,v.jsx)(`button`,{className:`image-bed-card-action`,type:`button`,onClick:()=>void q(e.url),title:`复制链接`,"aria-label":`复制链接`,children:(0,v.jsx)(l,{size:15})}),(0,v.jsx)(`a`,{className:`image-bed-card-action`,href:e.url,target:`_blank`,rel:`noreferrer`,title:`打开原图`,"aria-label":`打开原图`,children:(0,v.jsx)(h,{size:15})}),(0,v.jsx)(`button`,{className:`image-bed-card-action is-danger`,type:`button`,onClick:()=>void X(e),title:`删除图床文件`,"aria-label":`删除图床文件`,disabled:F===e.id,children:F===e.id?(0,v.jsx)(c,{className:`spin`,size:15}):(0,v.jsx)(g,{size:15})})]})]},e.path))}),T.length>D&&(0,v.jsxs)(`button`,{className:`btn-secondary image-bed-load-more`,type:`button`,onClick:()=>O(e=>Math.min(e+12,T.length)),children:[(0,v.jsx)(m,{size:16}),`加载更多`]})]}),(0,v.jsxs)(`details`,{className:`image-bed-help-section`,children:[(0,v.jsx)(`summary`,{children:`GitHub token 创建说明`}),(0,v.jsxs)(`p`,{className:`image-bed-help-intro`,children:[`这个图床固定使用当前 GitHub 账号下的公开仓库 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),`,图片会写入 `,(0,v.jsx)(`code`,{children:`images`}),` 目录并返回 raw.githubusercontent.com 原图链接。`]}),(0,v.jsxs)(`ol`,{className:`image-bed-steps`,children:[(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`先建公开仓库。`}),` 在 GitHub 新建仓库 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),`,Visibility 选 `,(0,v.jsx)(`strong`,{children:`Public`}),`。如果仓库已经存在,确认它属于这个 token 对应的个人账号,并且不是 Private。`]}),(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`创建 fine-grained token。`}),` 打开 GitHub 的 `,(0,v.jsx)(`a`,{href:`https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens`,target:`_blank`,rel:`noreferrer`,children:`Personal access tokens`}),` 页面,选择 `,(0,v.jsx)(`strong`,{children:`Fine-grained tokens`}),`,点 `,(0,v.jsx)(`strong`,{children:`Generate new token`}),`。`]}),(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`限制仓库范围。`}),` Token name 可填 `,(0,v.jsx)(`code`,{children:`AI Zero Token image bed`}),`,Expiration 按需设置,Resource owner 选自己的账号;Repository access 选择 `,(0,v.jsx)(`strong`,{children:`Only select repositories`}),`,只勾选 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),`。`]}),(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`给最小权限。`}),` 在 Repository permissions 里把 `,(0,v.jsx)(`strong`,{children:`Contents`}),` 设置为 `,(0,v.jsx)(`strong`,{children:`Read and write`}),`;`,(0,v.jsx)(`strong`,{children:`Metadata`}),` 保持默认 Read-only 即可,其它权限不需要打开。`]}),(0,v.jsxs)(`li`,{children:[(0,v.jsx)(`strong`,{children:`复制并验证。`}),` GitHub 只会展示一次生成后的 token,复制后回到这里粘贴,点击 `,(0,v.jsx)(`strong`,{children:`保存并验证`}),`。验证通过后就可以拖图上传。`]})]}),(0,v.jsxs)(`dl`,{className:`image-bed-help-facts`,children:[(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`dt`,{children:`Token 格式`}),(0,v.jsxs)(`dd`,{children:[`fine-grained token 通常以 `,(0,v.jsx)(`code`,{children:`github_pat_`}),` 开头。`]})]}),(0,v.jsxs)(`div`,{children:[(0,v.jsx)(`dt`,{children:`验证失败`}),(0,v.jsxs)(`dd`,{children:[`如果提示未找到仓库,优先检查仓库名是否为 `,(0,v.jsx)(`code`,{children:`azt-img-bed`}),`、仓库是否 Public、Repository access 是否选中了这个仓库。`]})]})]}),(0,v.jsx)(`p`,{className:`hint`,children:`Token 要像密码一样保管,不要发给别人。这个页面只会把它保存到你本机状态目录。`})]})]})}export{S as ImageBedPage};