wkgeo-cli 0.2.0 → 0.2.2

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
@@ -5,6 +5,8 @@
5
5
 
6
6
  * 技术形态:Node.js,**零第三方依赖**(只用 Node 内置模块),Node ≥ 20
7
7
  * 授权方式:OAuth 授权码 + **PKCE**(公共客户端,**代码里没有任何 client_secret**)
8
+ * 默认授权:**浏览器打开授权页 + 本地 `127.0.0.1` 回调**(等多久都不会自动改成设备码);设备码只在
9
+ 显式 `--device`、检测到系统代理、或本地回调端口真的起不来时兜底(见 §3.2)
8
10
  * 另一种授权:**设备码**(`login --device`,不需要回调地址,**对代理/防火墙免疫**;见 §3.2)
9
11
  * ⭐ **开着代理也能成功**:**只要检测到系统代理就直接改用设备码**(不看代理绕过列表),用户什么都不用做(见 §3.4)
10
12
  * 输出:人类可读 + **稳定 JSON**(默认非终端环境自动 JSON),失败也是结构化 JSON
@@ -143,10 +145,16 @@ wkgeo login
143
145
 
144
146
  于是"点同意"这件事**只需要 30 天做一次**(之后令牌过期或被吊销才需要重来)。
145
147
 
148
+ > ★ **0.2.2 起:默认就是这条本地回调路径**,`wkgeo login` **不会**因为"等了一会儿"
149
+ > 自动变成设备码。设备码只在三种情况出现:① 显式 `--device`;② 检测到系统代理
150
+ > (自动改道,`--no-auto-device` 可关,见 §3.4);③ 本地回调端口**真的起不来**
151
+ > (自动兜底,`--no-auto-device` 可关)。整段等待超时只报 `kind:"timeout"`,不再自动改道。
152
+
146
153
  选项:
147
154
 
148
155
  | 选项 | 说明 |
149
156
  | --- | --- |
157
+ | `--device` | 用**设备码模式**(RFC 8628):不需要本地回调地址/端口;本地读不了回调时的兜底(见 §3.2) |
150
158
  | `--no-browser` | **完全不尝试**自动打开浏览器(只打印网址,适合远程/无 GUI、自动化;★ 这是**唯一**的"不打开"开关) |
151
159
  | `--open-browser` | 强制尝试打开系统浏览器(⚠️ 现在与**默认行为等价** —— 默认就会尝试打开;保留只为显式语义与兼容,见下) |
152
160
  | `--scope "site:read site:write billing:self"` | 申请的能力范围(默认就是这三个,够建站+下单+支付) |
@@ -183,7 +191,12 @@ wkgeo login
183
191
  > (服务端只负责原样回传,防 CSRF 是客户端自己的责任)。
184
192
  > 端口被占用会自动换随机端口 —— 服务端按 RFC 8252 对回环地址**忽略端口**比对。
185
193
 
186
- ### 3.2 设备码模式(`--device`)—— **代理/防火墙环境的推荐路径**
194
+ ### 3.2 设备码模式(`--device`)—— **本地读不了回调 / 代理 / 远程机器时的兜底**
195
+
196
+ > ★ **默认不是设备码**:`wkgeo login` 默认走浏览器回调(§3.1)。设备码只在:
197
+ > ① 显式 `--device` / `--no-wait` / `--device-continue`;② 检测到系统代理(CLI 自动,§3.4);
198
+ > ③ 本地回调端口真的起不来(CLI 自动兜底,`reason:"callback_listen_failed"`)。
199
+ > **等待超时不会**自动改设备码(0.2.2 起)。
187
200
 
188
201
  ```bash
189
202
  wkgeo login --device # 申请短码 → 打印网址/短码 → 轮询到出结果(人盯着终端时用这条)
@@ -197,15 +210,20 @@ wkgeo login --device-continue # 用上次保存的设备码换票(配合 -
197
210
  ```
198
211
  请在浏览器中完成授权(设备码模式,不需要任何回调地址):
199
212
 
200
- 1) 打开:https://oauth.wkgeo.com/oauth/device/?user_code=ABCD-1234
213
+ 1) 打开:https://wkgeo.com/oauth/device/?user_code=ABCD-1234
201
214
  2) 如果页面没有自动填好短码,请输入:ABCD-1234
202
215
 
203
- 📋 可以直接复制这段发给 AI 端或转给用户:请打开 https://oauth.wkgeo.com/oauth/device/?user_code=ABCD-1234 并完成「同意授权」(短码:ABCD-1234)。
216
+ 📋 可以直接复制这段发给 AI 端或转给用户:请打开 https://wkgeo.com/oauth/device/?user_code=ABCD-1234 并完成「同意授权」(短码:ABCD-1234)。
204
217
  ```
205
218
 
219
+ > ★ **2026-10-18 域名统一到路径**:授权站 canonical 从 `https://oauth.wkgeo.com/` 改成
220
+ > **`https://wkgeo.com/oauth/`**(老地址继续可用)。CLI 以 `WKONGAI_OAUTH_WEB_BASE` 为 canonical,
221
+ > 服务端 metadata 给的授权地址只当"路径来源"用(详见 `lib/oauth-urls.mjs`);
222
+ > 回滚只要一条环境变量:`WKONGAI_OAUTH_WEB_BASE=https://oauth.wkgeo.com`。
223
+
206
224
  > ✅ 线上**已支持**这条路径(2026-10-02 实测:`metadata` 声明了 `device_authorization_endpoint`,
207
225
  > `grant_types_supported` 含 device_code,确认页 `/admin/oauth/device/` 也在)。
208
- > 端到端真跑记录见云函数测试目录的 `cloudfunctions/_tests/oauth-device-live-e2e.js`(默认只读,`OAUTH_DEVICE_E2E_LIVE=1` 才实跑)。
226
+ > 端到端真跑记录见云函数测试目录的 `tencent/functions/_tests/oauth-device-live-e2e.js`(默认只读,`OAUTH_DEVICE_E2E_LIVE=1` 才实跑)。
209
227
 
210
228
  #### `--no-wait` + `--device-continue`:给 **AI 端(Agent)** 用的"先给码、后领票"
211
229
 
@@ -216,7 +234,7 @@ AI 端是**一轮一轮**工作的:一次工具调用里挂着等 10 分钟会
216
234
  # ① Agent 调用(立刻返回,进程随即退出)
217
235
  wkgeo login --device --no-wait --json
218
236
  # → {"ok":true,"command":"login","stage":"awaiting_user","user_code":"ABCD-1234",
219
- # "verification_uri":"https://oauth.wkgeo.com/oauth/device/",
237
+ # "verification_uri":"https://wkgeo.com/oauth/device/",
220
238
  # "verification_uri_complete":"…/?user_code=ABCD-1234","paste_text":"…","expires_in":600,
221
239
  # "interval":5,"resume_command":"wkgeo login --device-continue","pending_file":"…","next_step":"…",
222
240
  # "user_action_required":"请让用户访问 … 并输入短码 ABCD-1234","no_wait":true}
@@ -255,7 +273,7 @@ wkgeo login --device-continue --json
255
273
  // 本地回调模式:第一行就给出网址,AI 可以立刻转述给用户
256
274
  // (注意 `authorize/` 的**尾斜杠**:托管侧对不带尾斜杠的目录路径会 302 且丢掉 query)
257
275
  {"ok":true,"command":"login","mode":"callback","stage":"awaiting_callback",
258
- "authorization_url":"https://oauth.wkgeo.com/oauth/authorize/?client_id=…",
276
+ "authorization_url":"https://wkgeo.com/oauth/authorize/?client_id=…",
259
277
  "redirect_uri":"http://127.0.0.1:53682/callback",
260
278
  "user_action_required":"请让用户在浏览器中打开上面的 authorization_url,登录并点「同意」"}
261
279
  // 结束(成功)
@@ -275,7 +293,7 @@ wkgeo login --device-continue --json
275
293
  多数代理默认绕过回环地址,但**全局/系统代理 + 绕过列表为空**时,浏览器会把这条回环请求
276
294
  也交给代理 → CLI 的本地服务永远收不到 `code`。**问题在浏览器那一侧,不在 CLI 出网**。
277
295
 
278
- **CLI 的处理(2026-10-02 第二版加固)**:
296
+ **CLI 的处理(2026-10-02 第二版加固;2026-10-21 默认回调调整)**:
279
297
 
280
298
  1. **检测**(只读,绝不改你的代理设置):Windows 读注册表 `…\Internet Settings` 的
281
299
  `ProxyEnable`/`ProxyServer`/`ProxyOverride`/`AutoConfigURL`;macOS 读 `scutil --proxy`;
@@ -295,27 +313,25 @@ wkgeo login --device-continue --json
295
313
  (检测详情:源=windows-registry 代理=127.0.0.1:7890 绕过列表=localhost,127.*,<local>;
296
314
  ⚠️ 绕过列表里就算写了 127.*/localhost 也不再作为放行回调的依据 —— 真机上出现过"写了却仍被代理接管"。)
297
315
  ```
298
- 4. ⭐ **回调模式"等太久"能自愈(第二条保险)**:★ **只在检测到系统代理时才启用** ——
299
- 有代理时首轮等待默认 **180 秒**(`--callback-first-wait 90` 或
300
- `WKONGAI_CALLBACK_FIRST_WAIT_MS=90000` 可显式指定,`0`/`off` 关闭):
301
- 这段时间没等到回调就**自动改用设备码继续**(不退出),不用再干等整段 `--timeout`
302
- (默认 300 秒,可到 900 秒)。
303
- ⚠️ **2026-10-03 修了两处**(老板真机反馈):
304
- * **没有代理时不再自愈**。以前它**无条件**挂在 60 秒上,而"用户把授权页读完再点同意"
305
- 经常超过 60 秒 ⇒ 明明没开任何代理也被切成设备码(原话:「现在变成设备码了,
306
- 我们因为没有开任何VPN,所以这个是不对的。」)。现在没代理就老老实实等满 `--timeout`,
307
- 只有你自己写了 `--callback-first-wait` 才按你写的来(**显式永远优先**)。
308
- * **切换设备码时不再关掉本地回调监听**:两条路**并存**、**哪条先成功就用哪条**。
309
- 旧版在切换那一刻就把监听关了,于是你**之后**才点【同意授权】时浏览器只会显示
310
- **ERR_CONNECTION_REFUSED**("无法访问此网站")——你会以为授权失败,其实马上就成功了。
311
- 现在那次点击照样能完成登录;若设备码先成功,之后到达的回调会看到一张
312
- 「本次登录已经完成」的页面。
313
- 本地回环端口**起不来**(防火墙/安全软件)时同样自动降级;
316
+ 4. ⭐ **等待超时不再自动改设备码(0.2.2,老板 2026-10-21 反馈)**:
317
+ 老板原话「默认的也不应该是设备授权,默认的就是那个传统的授权方式(浏览器打开授权页 +
318
+ 本地 localhost 回调)。设备码是本地读不了/代理时才用的。」
319
+ ⇒ 没有代理时 `login` 会**一路等回调到 `--timeout`**(默认 300 秒);超时只报
320
+ `kind:"timeout"`,并在 `hint` 里给出"显式改用 `--device`"的下一步,**不会**偷偷改道。
321
+ ⚠️ 历史行为(0.2.1 及更早):有代理时"首轮等待 180 秒"到点会改设备码、整段超时也会改。
322
+ 现在这个"首轮等待"**默认关闭**:只有你**显式**写 `--callback-first-wait 90` 或
323
+ `WKONGAI_CALLBACK_FIRST_WAIT_MS=90000` 才启用(**显式优先**,`reason:"callback_first_wait"`)。
324
+ 显式启用时仍然是**两条路并存**:本地回调监听**继续保留**,**哪条先成功就用哪条** ——
325
+ 你之后才点【同意授权】照样能完成登录。旧版在切换那一刻就把监听关了,于是你之后点同意
326
+ 浏览器只会显示 **ERR_CONNECTION_REFUSED**("无法访问此网站",2026-10-03 已修);
327
+ 若设备码先成功,之后到达的回调会看到一张「本次登录已经完成」的页面。
328
+ 5. ⭐ **本地回调端口真的起不来**(防火墙/安全软件/端口非法)→ 自动兜底设备码
329
+ (`reason:"callback_listen_failed"`),**不会**直接失败退出;
314
330
  **换票失败、用户点了拒绝这类"已经走到下一步"的错误不会降级**(避免让你白做一遍)。
315
- 5. **不想被自动改道**:加 `--no-auto-device`,或设 `WKONGAI_AUTO_DEVICE=0`。
331
+ 6. **不想被自动改道**:加 `--no-auto-device`,或设 `WKONGAI_AUTO_DEVICE=0`。
316
332
  ⚠️ 此时 CLI 会**明确警告**"若代理会接管 127.0.0.1,点了【同意授权】也收不到结果",
317
- 并且**关掉**第 4 条的自愈(你要求只走回调,就不会背着你偷偷改道)。
318
- 6. **排障入口**:`wkgeo config` 会直接打印本机代理情况与结论(`--json` 里是
333
+ 并且**关掉**第 4 条的自愈与第 5 条的端口兜底(你要求只走回调,就不会背着你偷偷改道)。
334
+ 7. **排障入口**:`wkgeo config` 会直接打印本机代理情况与结论(`--json` 里是
319
335
  `data.proxy` / `meta.proxy_warning`),支持同学让用户跑这一条就能判断。
320
336
 
321
337
  `--json` 里的结构化字段(AI 端据此判断"是不是代理的锅、该让用户做什么"):
@@ -341,6 +357,13 @@ wkgeo login --device-continue --json
341
357
  失败时的 `error.hint` 里也会带上同样的三选一(例如回调超时的 JSON 错误信封里,
342
358
  `meta.proxy_warning` 依然在)。
343
359
 
360
+ > ★ **0.2.2 兼容字段**:`data.auto_device` 形状**不变**(`{applied, reason}`),`mode` 仍是
361
+ > `"callback"` / `"device"`,**没有删除任何既有字段**。`reason` 现在只会是:
362
+ > `"proxy_detected"`(检测到代理)/ `"callback_listen_failed"`(本地端口起不来)/
363
+ > `"callback_first_wait"`(仅用户显式 `--callback-first-wait` 时)/ `null`。
364
+ > 历史取值 `"callback_timeout"` **不再产生**(0.2.2 起超时不改道;老脚本把它当"回调失败后的
365
+ > 设备码兜底"读到的语义不变)。
366
+
344
367
  > ⚠️ 两个容易误读的点:
345
368
  > 1. **CLI 自己的出网不受系统代理影响**:Node 的 fetch(undici)默认既不读系统代理、
346
369
  > 也不读 `HTTP_PROXY`(要 Node ≥ 24 的 `--use-env-proxy` / `NODE_USE_ENV_PROXY=1`)。
@@ -359,13 +382,32 @@ wkgeo login --device-continue --json
359
382
 
360
383
  | 命令 | 作用 | 关键参数 |
361
384
  | --- | --- | --- |
362
- | `login` | 授权一次,令牌存本地 | `--device`、`--no-wait`、`--device-continue`、`--auto-device`/`--no-auto-device`、`--callback-first-wait <秒>`、`--no-browser`、`--open-browser`、`--scope`、`--callback-port`、`--timeout <秒>` |
385
+ | `login` | 授权一次,令牌存本地(**默认浏览器回调**;设备码仅兜底) | `--device`、`--no-wait`、`--device-continue`、`--auto-device`/`--no-auto-device`、`--callback-first-wait <秒>`(可选,默认关闭)、`--no-browser`、`--open-browser`、`--scope`、`--callback-port`、`--timeout <秒>` |
363
386
  | `whoami` | 当前登录的是谁(账号标识) | `--verbose`(额外显示令牌**掩码**与文件权限) |
364
387
  | `logout` | 删本地凭据(默认还调服务端吊销) | `--local-only` |
365
388
  | `config` | 显示生效配置与登录状态(无令牌) | — |
366
389
  | `sites list` | 我的站点 | `--limit`、`--offset` |
367
390
  | `sites create` | 建站(**建在令牌所属用户名下**) | `--name`(必填)、`--customer`、`--template`、`--domain` |
368
391
  | `sites delete` | 删站(服务端是**软删除**,进回收站) | `<站点ID>` |
392
+ | `site use` | **选中要操作的站点**(本地记住,之后的 CMS 命令都带它;不给参数 = 看当前是哪个) | `<站点ID或域名>`、`--clear` |
393
+ | `categories list` | 栏目列表(树序) | — |
394
+ | `categories get` | 单个栏目详情(含栏目正文、父栏目字段) | `<栏目ID\|slug\|path\|栏目名>` |
395
+ | `categories create` | 新建栏目 | `--name`(必填)、`--parent`、`--kind channel\|list\|page`、`--slug`、`--status`、`--sort`、`--icon`、`--link-url`、`--seo-description`、`--seo-keywords`、`--sub-title`、`--no-in-menu`、`--body`/`--body-file` |
396
+ | `categories update` | 改栏目(只改给了的字段) | `<栏目>` + 上面任意字段 |
397
+ | `categories delete` | 删栏目(系统栏目/有子栏目或内容时被拒) | `<栏目>` |
398
+ | `categories bind-template` | 给栏目绑定模板 html(`--clear` = 解绑) | `<栏目>` `<模板文件>`、`--clear` |
399
+ | `contents list` | 内容列表(不含正文) | `--category`、`--status`、`--keyword`、`--page`、`--page-size`、`--sort views_desc`、`--include-children`、`--owner-page` |
400
+ | `contents get` | 单条内容详情(正文/附件/SEO/自定义字段) | `<内容ID>` |
401
+ | `contents create` | 新建内容(默认草稿) | `--title`(必填)、`--category`(必填)、`--status`、`--body`/`--body-file`、`--description`、`--keywords`、`--sub-title`、`--cover`、`--video`、`--usage`、`--attachments`、`--ext`、`--pinned`、`--sort` |
402
+ | `contents update` | 改内容(只改给了的字段) | `<内容ID>` + 上面任意字段(`--title` 也可) |
403
+ | `contents delete` | 删内容(「单页」那一页删不掉) | `<内容ID>` |
404
+ | `assets list` | 素材列表(默认只列图片;可看回收站) | `--kind`、`--keyword`、`--status normal\|deleted`、`--sort`、`--page`、`--page-size` |
405
+ | `assets upload` | 上传素材(**签名 → 直传 OSS → 登记**,CLI 不碰 AK) | `<本地文件>`、`--name` |
406
+ | `templates list` | 模板清单(默认模板 + 每个 html 的字符数) | — |
407
+ | `templates get` | 看模板源码(不给文件名 = 列清单;给了就直出源码) | `[模板文件名]` |
408
+ | `templates save` | 新建/覆盖模板 html(≤40 万字符) | `<文件名.html>` + `--file <本地文件>` 或 `--content <源码>` |
409
+ | `publish` | **看发布状态**(前台按需渲染:`published` 即上线,没有额外发布步骤) | — |
410
+ | `import` | 老站导入(**默认 dry-run**;走本机运维脚本,需运维权限) | `--url`(必填)、`--apply`、`--site`、`--status`、`--out`、`--max-pages`、`--delay`、`--include`、`--exclude`、`--overwrite`、`--no-probe` |
369
411
  | `wallet` | 余额 + 最近流水 | `--limit`、`--entitlements` |
370
412
  | `wallet txns` | 钱包流水(普通用户自己的那份) | `--limit` |
371
413
  | `wallet recharge` | **充值下单**:返 `code_url` + **`pay_url`(自家收银台页面,带订单支付令牌 `token`;★ 默认会自动尝试用系统浏览器打开,`--no-browser` 可关;**支付成功后页面会自己关掉**)**(**不会自动扣款**) | `--amount-cents`(必填,单位分)、`--no-browser`、`--open-browser`、`--dry-run` |
@@ -379,6 +421,67 @@ wkgeo login --device-continue --json
379
421
  | `apps revoke` | 一键吊销某应用的授权(**令牌立即失效**) | `<client_id>` |
380
422
  | `skill` | **打印给 AI 端的操作手册**(`SKILL.md` 原文,随包分发) | `--json`(默认**纯文本原文**,不受"非终端自动 JSON"影响) |
381
423
 
424
+ ### CMS 命令(栏目 / 内容 / 素材 / 模板)怎么用
425
+
426
+ **三步就够**(AI 端也是这三步):
427
+
428
+ ```bash
429
+ wkgeo site use www.example.com # ① 选站(本地记住;也可每条命令加 --site <站点ID>)
430
+ wkgeo categories list # ② 看结构
431
+ wkgeo contents create --category 新闻 --title "标题" --status published --body "<p>正文</p>"
432
+ wkgeo assets upload ./banner.jpg # 传图 → 拿素材 ID → 再 --cover 挂到内容上
433
+ wkgeo templates list / get / save # 改版式(后端动作已就绪)
434
+ wkgeo publish # ③ 看状态(见下)
435
+ ```
436
+
437
+ > 只属于**一个**站点的账号可以跳过 ①:不带 `x-cms-site` 时服务端会用那唯一一个成员站;
438
+ > 有多个成员站又不指定 → 报 `cms_site_required` 并教你选(不替你猜,免得改了 A 站以为改的是 B 站)。
439
+
440
+ **接口契约(写代码/排障时看这张表)**
441
+
442
+ | 项 | 值 |
443
+ | --- | --- |
444
+ | 地址 | `POST https://api.wkgeo.com/cms/<action>`(`--cms-base` / `WKONGAI_CMS_BASE` 可改;老写法 `/api/cms/<action>` 继续兼容) |
445
+ | 动作名 | 就是云函数动作表的键:`categoryList` / `categorySave` / `contentList` / `contentSave` / `assetSign` / `assetRegister` / `templateGet` / `templateFileSave` / `overview` … |
446
+ | 鉴权 | `Authorization: Bearer <OAuth 令牌>`(从本地凭据取;**不是** Cookie,也不存第二套账号) |
447
+ | 站点 | `x-cms-site: <站点ID>`(**必须是 id**;传域名会被后端当成"没指定")。**没选站时这个头不带** —— 服务端按你的成员关系判定(单站自动走、多站报 400)。body 里**不传** `site_id`(后端故意忽略) |
448
+ | 响应 | 统一信封 `{code, message, data}`,`code === 0` 才算成功(与 `/api/data` 同一个约定) |
449
+
450
+ > ✅ **认证桥已上线(2026-10-18 下午部署,已实测)**:CMS 云函数现在认
451
+ > `Authorization: Bearer <OAuth 令牌>` → 用户 → `cms_site_member`,
452
+ > **与浏览器同一套角色/权限**(额外受 OAuth scope 收紧)。
453
+ > 实测证据(只读命令、真实站点):
454
+ > * `wkgeo categories list --site <站点ID>` → `ok:true`(返回 17 个栏目);
455
+ > * `wkgeo contents list --site <站点ID> --page-size 3` → `ok:true`(`total: 6`);
456
+ > * `wkgeo publish --site <站点ID>` → `ok:true`(读到 6 条已发布);
457
+ > * 不带站点 → 服务端回 `400 这个账号有 N 个站点,请用 x-cms-site 头…` → CLI 译成 `cms_site_required`;
458
+ > * 站点 id 不存在 / 不是成员 → `403` → CLI 译成 `cms_not_member`。
459
+ >
460
+ > 桥上线后 **401 只剩一种含义:令牌无效**(过期/被吊销/客户端被停用)。此时 CLI 仍会给一段
461
+ > 两可的提示(也覆盖"旧部署没启用桥"那种情况),但正常环境下照它 ① 重新 `login` 即可:
462
+ >
463
+ > ```json
464
+ > {"ok":false,"command":"categories list","error":{
465
+ > "kind":"cms_auth","code":401,
466
+ > "message":"CMS 接口没有接受 CLI 的授权令牌(categoryList):…",
467
+ > "hint":"① 最常见的原因是令牌已过期 / 被吊销:运行 `wkgeo login` 重新授权即可;② 若刚授权仍报 401(且 `wkgeo whoami` 正常),说明服务端是**旧部署** —— 还没启用 CLI 认证桥…"},
468
+ > "meta":{"cms_base":"https://api.wkgeo.com/cms","site_file":"…\\.wkongai\\config.json"}}
469
+ > ```
470
+ >
471
+ > 退出码 **3**(与"需要授权"同一档),AI 端据此就能判断"该重新授权"。
472
+ >
473
+ > ⚠️ **升级前授权的旧令牌缺少 CMS 的 scope**(`template:read/write`、`attachment:read/write`):
474
+ > 表现是 `templates list` / `assets list` 报 `cms_scope`(403)。
475
+ > **重新 `wkgeo login` 一次即可** —— CLI 现在的默认 scope 已含这三组,客户端 `allowed_scopes` 也已放开。
476
+
477
+ **输出与安全**
478
+
479
+ * 默认人类可读表格;`--json`(非终端下自动)输出**一个** JSON 信封,`data` 里是原始字段(AI 直接用);
480
+ * `templates get <文件名>` 在人类模式下**直出模板源码**(可以 `> index.html` 存下来);
481
+ * **不打印令牌**;`assets upload` 也不打印 OSS 的 AccessKey(CLI 只拿票据,AK 只在云函数里);
482
+ * 服务端的所有约束**一条都不会被 CLI 放宽**:系统栏目只能隐藏不能删、单页内容只能改不能建、
483
+ 绑定模板必须是本站已有的 html、内容 `published` 即上线(前台是服务端按需渲染)。
484
+
382
485
  ### 「钱包余额支付」到底发生在哪一步(**别被命令名骗了**)
383
486
 
384
487
  服务端 `placeOrder()` 的行为是:**下单时就先用余额抵扣**,余额够就立刻 `moveWallet(-金额)` +
@@ -448,6 +551,14 @@ wkgeo login --device-continue --json
448
551
  | `code≥500` | `server` | 1 | 服务端错误,保留原文;可稍后重试 |
449
552
  | 连不上/超时 | `network` | 1 | 检查网络;原始 message 会原样保留(如 `getaddrinfo ENOTFOUND`) |
450
553
  | 动作未登记(新功能没发版) | `not_deployed` | 1 | **服务端缺口**,不是命令用错(`apps list/revoke` 已实测可用) |
554
+ | CMS 命令:`code=401`(Bearer 令牌未被接受) | `cms_auth` | 3 | 先确认 `wkgeo whoami` 正常;**正常就说明服务端还没启用 CLI 认证桥**(令牌 → CMS 站点成员),找管理员,别反复 `login` |
555
+ | CMS 命令:网关 404 / 响应不是 JSON | `cms_unavailable` | 1 | 多半是 CMS 基址不对或云函数 `cms` 没部署;`wkgeo config` 里看 `cms_base`(`--cms-base` 可临时改) |
556
+ | CMS 命令:403「你不是该站点的成员」 | `cms_not_member` | 1 | 站点选错了 / 还没被邀请:`wkgeo site use <正确站点ID>`;协作站要 owner 先加你 |
557
+ | CMS 命令:403「只读账号不能修改…」 | `cms_readonly` | 1 | 你的站点角色是 viewer:让 owner 在后台「用户与角色」里升为 editor |
558
+ | CMS 命令:403「该令牌没有执行「X」的权限(需要 site:write)」 | `cms_scope` | 1 | 令牌 scope 不够:**重新 `wkgeo login`**(默认已含 site / template / attachment 三组)。升级前的旧令牌不会自动带上新能力 |
559
+ | CMS 命令:400「这个账号有 N 个站点,请用 x-cms-site…」 | `cms_site_required` | 1 | 多站账号必须指定站点:`wkgeo site use <站点ID>` 或本命令加 `--site <站点ID>` |
560
+ | `import`:找不到本机导入脚本 | `import_unavailable` | 1 | 只有本仓库机器上才有 `tencent/cloudbase/import-site.mjs`;或用 `WKONGAI_IMPORT_SCRIPT` 指定路径 |
561
+ | `import`:脚本非 0 退出 | `import_failed` | 1 | 看报错里的日志尾巴(url 打不开 / 目标站不存在 / 没有数据库凭证 / 触发 robots 限制);dry-run 失败不动库 |
451
562
 
452
563
  **所有这些错误都不会打印令牌**:错误里只带 `code` / `message` / `hint`。
453
564
 
@@ -460,6 +571,7 @@ wkgeo login --device-continue --json
460
571
  | 位置 | `~/.wkongai/credentials.json`(Windows:`%USERPROFILE%\.wkongai\credentials.json`);可用 `WKONGAI_CREDENTIALS` 或 `--credentials` 改 |
461
572
  | 权限 | 目录 `0700`、文件 **`0600`**。⚠️ 这在 POSIX 上有效;**Windows 上 NTFS 的 `chmod` 基本是空操作**,实际靠用户目录 ACL 保护 —— 这点如实写明,不假装在 Windows 上也成立(`whoami --verbose` 会显示"无法判断(Windows)") |
462
573
  | 内容 | `access_token` / `refresh_token` / `scope` / `expires_at` / 账号标识;写入用**原子写**(先写临时文件再 rename,避免半截 JSON) |
574
+ | 站点选择 | **另一个文件**:`<凭据目录>/config.json`(默认 `~/.wkongai/config.json`,`wkgeo site use` 写入)。它只是"操作上下文",**不含秘密**;`logout` 不会删它,重新 `login` 也不会丢 |
463
575
  | 令牌 | 30 天有效(`expires_in=2592000`)。CLI **不会**自动刷新(老板口径:过期就重新授权,30 天已经够长) |
464
576
  | 绝不出现明文令牌的地方 | 终端输出(含 `--verbose`,只给**掩码** `AbCdEf…(43 位)`)、错误信息、JSON 输出、日志 |
465
577
  | 公共客户端 | 代码里**没有 client_secret**,只用 PKCE(`code_verifier` 只在内存里,换票后就丢) |
@@ -494,7 +606,7 @@ wkgeo login --device-continue --json
494
606
  | --- | --- | --- |
495
607
  | 1 | `wkgeo wallet`(记下当前余额,例如 ¥100.00) | 打印余额与流水 |
496
608
  | 2 | `wkgeo wallet recharge --amount-cents 1000 --dry-run` | 只打印"将创建 ¥10.00 订单",**不产生任何订单** |
497
- | 3 | `wkgeo wallet recharge --amount-cents 1000` | 返回 `order_id` / `code_url` / **`pay_url`**,并**自动在浏览器打开 `pay_url`** —— 也就是我们自己的收银台 `https://pay.wkgeo.com/pay/?order_id=…&token=…` |
609
+ | 3 | `wkgeo wallet recharge --amount-cents 1000` | 返回 `order_id` / `code_url` / **`pay_url`**,并**自动在浏览器打开 `pay_url`** —— 也就是我们自己的收银台 `https://pay.wkgeo.com/?order_id=…&token=…` |
498
610
  | 4 | 在**收银台页面**用手机微信扫码支付 ¥10.00(人工) | 页面自动检测到支付成功;**支付成功后页面会自动尝试关闭**(关不掉就手动关掉即可);`pay_url` 里的订单号在任何设备/浏览器打开都能看到同一笔单 |
499
611
  | 5 | `wkgeo orders check <order_id>` | `paid:true` + **最新余额 = 原余额 + 1000 分** |
500
612
  | 6 | `wkgeo wallet txns` | 出现一条 `kind=recharge`、金额 `+¥10.00` 的流水 |
@@ -527,7 +639,8 @@ wkgeo login --device-continue --json
527
639
 
528
640
  ### 路径 ③ 网页端能看到账户信息
529
641
 
530
- 浏览器打开 `https://user.wkgeo.com/` → 登录同一账号 → 依次核对:
642
+ 浏览器打开 `https://wkgeo.com/user/`(老地址 `https://user.wkgeo.com/` 同样可用)
643
+ → 登录同一账号 → 依次核对:
531
644
 
532
645
  - **个人中心/账号信息**:邮箱或手机号、昵称与 CLI 里 `whoami` 显示的一致;
533
646
  - **钱包/账单**:余额 = `wkgeo wallet` 的余额;订单列表里能看到 CLI 下的单(同一个 `订单号`);
@@ -593,10 +706,10 @@ node scripts/live-sql.mjs --sql "select …" # 只读查库(只允许 SELECT
593
706
  设备码这条路的**真环境**端到端验证在云函数测试目录(默认只读,实跑需显式开关):
594
707
 
595
708
  ```bash
596
- node cloudfunctions/_tests/oauth-device-live-e2e.js # 只读预检(零副作用)
709
+ node tencent/functions/_tests/oauth-device-live-e2e.js # 只读预检(零副作用)
597
710
  $env:OAUTH_DEVICE_E2E_LIVE='1' # 实跑:造临时用户 + 真跑 CLI
598
711
  $env:OAUTH_DEVICE_E2E_CONFIRM='yes-i-accept-creating-a-temp-user'
599
- node cloudfunctions/_tests/oauth-device-live-e2e.js # 结束按 sub 全量清理并回读复核
712
+ node tencent/functions/_tests/oauth-device-live-e2e.js # 结束按 sub 全量清理并回读复核
600
713
  ```
601
714
 
602
715
  环境变量(都有默认值,通常不用设):
@@ -604,16 +717,16 @@ node cloudfunctions/_tests/oauth-device-live-e2e.js # 结束
604
717
  | 变量 | 默认 | 说明 |
605
718
  | --- | --- | --- |
606
719
  | `WKONGAI_BASE` | 线上网关 | API 基址 |
607
- | `WKONGAI_OAUTH_WEB_BASE` | `https://oauth.wkgeo.com` | **授权站**基址(同意页 / 设备码页兜底地址) |
608
- | `WKONGAI_PAY_WEB_BASE` | `https://pay.wkgeo.com` | **收银台**基址(`pay_url` 的域名) |
609
- | `WKONGAI_USER_WEB_BASE` | `https://user.wkgeo.com` | **会员站**基址(登录页 / 我的订单等) |
720
+ | `WKONGAI_OAUTH_WEB_BASE` | `https://wkgeo.com/oauth` | **授权站**基址(同意页 / 设备码页兜底地址)。2026-10-18 域名统一到路径后默认走新路径;老地址 `https://oauth.wkgeo.com` 仍可用,回滚就把它设成老地址 |
721
+ | `WKONGAI_PAY_WEB_BASE` | `https://pay.wkgeo.com` | **收银台**基址(`pay_url` 的域名;**支付独立,路径化没动**) |
722
+ | `WKONGAI_USER_WEB_BASE` | `https://wkgeo.com/user` | **会员站**基址(登录页 / 我的订单等)。老地址 `https://user.wkgeo.com` 仍可用 |
610
723
  | `WKONGAI_WEB_BASE` | 未设置 | ⚠️ **总覆盖**:设了它上面三个基址**全部**用它(联调 / 回滚用)。它**不再**指向老站 |
611
724
  | `WKONGAI_CLIENT_ID` | `wkgeo-cli` | OAuth 客户端 id |
612
725
  | `WKONGAI_SCOPE` | `site:read site:write billing:self` | 申请的能力 |
613
726
  | `WKONGAI_CREDENTIALS` | `~/.wkongai/credentials.json` | 凭据文件路径 |
614
727
  | `WKONGAI_CALLBACK_PORT` | `53682` | 本地回调端口 |
615
728
  | `WKONGAI_LOGIN_TIMEOUT_MS` | `300000` | 等待授权超时(⚠️ 变量名里带 `MS`,所以这里填**毫秒**;命令行 `--timeout` 填的是**秒**) |
616
- | `WKONGAI_CALLBACK_FIRST_WAIT_MS` | 未设置 | ★ 回调模式的**首轮等待**(毫秒):显式设了它(或命令行 `--callback-first-wait`)这段时间没等到回调就自动改用设备码继续(**本地回调监听会同时保留**);`0` 关闭(等价于 `--callback-first-wait 0`)。⚠️ **未设置时看代理**:检测到系统代理才用默认值 **180000**(180 秒),**没有代理则不启用**(这是 2026-10-03 修掉的"没开 VPN 也被切成设备码") |
729
+ | `WKONGAI_CALLBACK_FIRST_WAIT_MS` | 未设置 | ★ **可选**的"首轮等待"(毫秒,**默认关闭**):只有显式设了它(或命令行 `--callback-first-wait`),这段时间没等到回调才会自动改用设备码继续(**本地回调监听会同时保留**);`0` 关闭(等价于 `--callback-first-wait 0`)。⚠️ **未设置时一律不启用**(0.2.2 起:等待超时不再自动改设备码;老板 2026-10-21:「默认的就是那个传统的授权方式」) |
617
730
  | `WKONGAI_AUTO_DEVICE` | `1`(开) | 检测到系统代理时是否自动改用设备码;设 `0`/`false`/`no`/`off` 关闭(等价于 `--no-auto-device`,会打印"代理可能吃掉回调"的警告) |
618
731
  | `NO_COLOR` | — | 设了就不上色 |
619
732
 
@@ -624,7 +737,7 @@ node cloudfunctions/_tests/oauth-device-live-e2e.js # 结束
624
737
  | # | 项 | 现状 | 谁来处理 |
625
738
  | --- | --- | --- | --- |
626
739
  | 1 | **设备码端点** | ✅ **已上线并端到端真跑通过**(2026-10-02:metadata 声明 + 申请/确认/换票/重放全绿,真 CLI 子进程 `stage:"done"`)。协议细节见 `docs/设备码授权-服务端最小方案.md`(该文档已更新为"已实现") | 已完成;回归用 `_tests/oauth-device-live-e2e.js` |
627
- | 1b | **代理自动降级 / 首轮等待自愈** | ✅ 已实现(**检测到代理即改设备码**,不再相信绕过列表 + 整段超时/端口失败自动改道);2026-10-02 **真机翻车已复现并加固**(老板机器:绕过列表写了 `127.*` 却仍被代理接管,证据 `curl -x …7890 http://127.0.0.1:1/x` → 502)。★ 2026-10-03 **真机反馈已修**:① 首轮等待自愈**只在检测到代理时启用**(无代理不再误切设备码)、默认 180 秒、显式 `--callback-first-wait` 永远优先;② 切设备码时**保留本地回调监听**,两条路并存先到先得(旧版关掉监听 ⇒ 用户之后点【同意授权】得到 ERR_CONNECTION_REFUSED) | 已完成(单测 ①~⑤ 含鉴别力自证);建议老板在**没开代理**的机器上跑一次 `wkgeo login` 验收"不再自动变成设备码" |
740
+ | 1b | **代理自动降级 / 默认回调** | ✅ 已实现(**检测到代理即改设备码**,不再相信绕过列表;本地回调端口起不来自动兜底)。2026-10-02 **真机翻车已复现并加固**(老板机器:绕过列表写了 `127.*` 却仍被代理接管,证据 `curl -x …7890 http://127.0.0.1:1/x` → 502)。★ 2026-10-21 **默认模式已改回传统回调**(0.2.2):**等待超时不再自动改设备码**;设备码只由①显式 `--device`②检测到代理③本地回调端口真的起不来触发;`--callback-first-wait` 变成**默认关闭**的显式可选自愈;切设备码时**保留本地回调监听**,两条路并存先到先得(旧版关掉监听 ⇒ 用户之后点【同意授权】得到 ERR_CONNECTION_REFUSED) | 已完成(单测:默认 callback / `--device` 强制 / 端口失败兜底 / `--no-auto-device` 不兜底 / 超时不改道);建议老板真机跑一次 `wkgeo login` 验收"默认就是浏览器回调" |
628
741
  | 1c | **设备码页"没有完成态/不自动关"** | ⚠️ 老板 2026-10-02 反馈:点完设备码页的【同意授权】后页面**不给明确的完成态、也不自动关**,用户不知道要不要继续等。**并行改动**已在 `admin-web/src/pages/device/index.tsx` 补上终态卡片(`✅ 授权成功` + "本页面可以关闭了" + 返回首页)。本轮 CLI 侧**未改前端** | 见下面 §9.1 的补充建议(可选) |
629
742
  | 2 | **错误码 40101/40102** | `/api/data` **已返回 40102**(实测"访问令牌已被吊销,请重新授权");但 `userinfo` 仍是标准 `401 invalid_token`(靠文案区分)。CLI 两条路都兼容 | 服务端(可选,统一到 40101/40102) |
630
743
  | 3 | **`listMyOAuthAuthorizations.token_count` 虚高** | 该 SQL 用 `count(*)` 统计 `left join lateral unnest(t.scopes)` 后的行数 → **数的是 scope 个数,不是令牌数**(实测:1 把令牌、3 个 scope → 显示"3 把")。正确写法 `count(distinct t.access_token_hash)` | **主线程/服务端** |
@@ -636,6 +749,11 @@ node cloudfunctions/_tests/oauth-device-live-e2e.js # 结束
636
749
  | 9 | **真实微信支付全流程** | 按红线**没有**跑真钱:`wallet recharge` 只用离线桩验证了请求体,真钱路径由老板手动测(§7 路径②) | 人工 |
637
750
  | 10 | **exe 产物** | 只验证了 `--sea` 能生成 blob + 复制 node 壳(93MB),**没有**真装 postject 注入、也没在无 Node 机器上跑 | 需要时再做 |
638
751
  | 11 | **`login` 的浏览器交互本身** | 真环境验证脚本扮演了"浏览器/同意页"的角色(调 authorize/consent),**HTML 页面本身的点击**没有被自动化覆盖(同意页由前端/另一个改动负责) | 人工(§7 路径①) |
752
+ | 12 | **CMS 认证桥(Bearer → 站点成员)** | ✅ **已上线并实测**(2026-10-18 下午):`categories list` / `contents list` / `publish` 用 Bearer + `x-cms-site` 真跑成功;多站未指定 → 400(CLI 译成 `cms_site_required`)。⚠️ 15:15 首次实测时线上还是旧函数(401 老文案),15:22 复测已通 —— 排障时先看 401 的**文案** | 已完成(后端 `resolveIdentity`) |
753
+ | 13 | **CMS 写操作 / 导入没有真接口验证** | 只读命令已真跑;**写操作**(建栏目/发内容/传素材/存模板)与**导入**没有真跑(会改数据/需数据库凭证)。它们由 66 条离线单测覆盖,**不声称已端到端验证** | 需要时人工验收(写库前先 `site use` 选对站) |
754
+ | 13b | **旧令牌缺 CMS scope** | 2026-10-18 之前授权的令牌只有 `site:read/site:write/billing:self` ⇒ `templates list` / `assets list` 报 `cms_scope`(403)。**重新 `wkgeo login` 即可**(默认 scope 与客户端 `allowed_scopes` 都已补齐) | 用户侧一次重新授权 |
755
+ | 14 | **`import` 走本机运维脚本** | 后端**没有**带鉴权的 HTTP 导入入口,CLI 调用本仓库的 `tencent/cloudbase/import-site.mjs`(`service_role` 直连库,需运维权限)。命令默认 dry-run、只增不毁;客户/AI 端不应当用它 | 后端补 HTTP 导入动作(`docs/规划-CLI与AI建站.md` §5.2) |
756
+ | 15 | **静态发布 / 客户自有域名** | `publish` 只报状态并解释"渲染即生效",**不做**静态化与 CDN 推送 —— 缺"按站点 ID 幂等发布"的后端动作 | 后端(`规划-Agent与开放接口.md` 第 4 条) |
639
757
 
640
758
  ### 9.1 ⚠️ 设备码页"没有完成态 / 不自动关"(**建议改法,本轮 CLI 侧未改动**)
641
759
 
@@ -679,3 +797,20 @@ A:服务端设计如此:下单时余额够就直接扣款并履约(返回
679
797
  **Q:`sites create` 报「无法确定所属客户」?**
680
798
  A:这是账号属性问题(`owner/admin` 这类 `data_scope != 'self'` 的账号必须显式指定客户):
681
799
  加 `--customer <客户ID>` 重试(客户 ID 在网页后台的客户列表里)。
800
+
801
+ **Q:`categories list` 报 401 / `cms_auth`?**
802
+ A:认证桥**已上线**(2026-10-18 下午实测通过),所以 401 现在基本只有一种含义:**令牌无效**
803
+ (过期 / 被吊销 / 客户端被停用)→ `wkgeo login` 重新授权即可。
804
+ 只有当服务端还是**旧部署**(没启用 Bearer 桥)时,才会出现"`whoami` 正常、CMS 却一律 401",
805
+ 这时才需要找管理员确认部署(CLI 侧不用改,也不用反复登录)。
806
+
807
+ **Q:CMS 命令报 `cms_site_required`(账号有多个站点)?**
808
+ A:那就是"没指定站点、而你有多个成员站"——服务端不替你猜。
809
+ 先选一次:`wkgeo site use <站点ID或域名>`(存进 `~/.wkongai/config.json`);
810
+ 多站来回切时不必改本地记录:任意命令加 `--site <站点ID>` 只影响这一次。
811
+ ⚠️ `--site` **只认站点 ID(uuid)**;域名请先用 `site use <域名>` 解析。
812
+ (只属于**一个**站点的账号不用选:不带站点头时服务端会直接用那唯一一个。)
813
+
814
+ **Q:`assets upload` 会让我填 OSS 的 AK 吗?**
815
+ A:**不会**。流程是"云函数签一张 10 分钟的直传票据 → CLI 按票据把文件 POST 到 OSS → 回云函数登记"。
816
+ CLI 侧既不保存也不打印 AccessKey(和"桌面/CLI 永远拿不到 AK"同一条边界)。