@xqyz/xq-cli 0.3.8 → 0.3.10

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/CHANGELOG.md CHANGED
@@ -10,6 +10,41 @@
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ (暂无未发布变更)
14
+
15
+ ## [WorkBuddy Plugin 0.10.24 / SkillHub 1.0.8] - 2026-08-26
16
+
17
+ ### MCP 中文展示名
18
+
19
+ - MCP 初始化信息的用户可见名称改为“喜鹊标书”,同时保留 `xique-bid` 内部标识、工具命名空间和配置升级键不变。
20
+
21
+ ### CLI 本地登录页视觉升级
22
+
23
+ - 登录页改为响应式双栏工作台,补充一次登录复用、分阶段工作流和 Key 轮换等接入信息。
24
+ - API Key 说明改为“凭证保存在本机,后续命令无需重复输入”,保留 API Key 登录与浏览器授权两条入口及原有提交接口。
25
+
26
+ ### CLI 登录页支持 API Key
27
+
28
+ - `xq-cli login` 默认打开本机登录页,普通用户可在页面中选择浏览器授权或输入 API Key。
29
+ - API Key 只通过本机回环地址提交,由 CLI 调用 `/user/info` 验证后保存;不会进入 URL 或日志。
30
+ - 保留 `xq-cli login --browser` 直接打开浏览器授权,以及 `--no-open` 手动复制本机页面地址。
31
+
32
+ ### WorkBuddy Skill 自助更新
33
+
34
+ - 用户只需说“更新喜鹊标书”或“升级喜鹊标书”,即可授权拉取正式 SkillHub `@org-kkw9656k/xique-ai`,无需重复粘贴完整安装提示。
35
+ - 安装器优先下载并校验 SkillHub 最新包;远端不可用、版本落后或要求更高运行版本时使用兼容的内置/已安装 Skill,不覆盖现有新版本。
36
+ - SkillHub 下载只接受经过身份、版本、路径、文件类型和大小校验的 Skill Markdown 内容;更新仍会备份旧 Skill,并在新会话中生效。
37
+
38
+ ## [0.3.9 / WorkBuddy Plugin 0.10.23 / SkillHub 1.0.6] - 2026-08-26
39
+
40
+ ### WorkBuddy 规划模式自动推进
41
+
42
+ - 修复规划模式解析已经达到 `success=3`,但后端未在任务详情回显标包和 `planMode` 时被误判为 `TASK_PARSE_NOT_ADVANCED` 的问题。
43
+ - WorkBuddy 检测到 `analysis_ready` 后自动对同一 `cid` 执行 `plan confirm-outline`,不再展示 `analysis-review` 或智能解读确认卡。
44
+ - 已被旧版本误标失败、但错误中已记录解析 `success=3` 的规划任务,在查询同一 `runId` 时会原地恢复并自动确认生成大纲。
45
+ - 保留大纲和目录人工确认闸门;自动推进只覆盖后台智能解读到大纲生成这一段,不会提前生成正文。
46
+ - WorkBuddy Plugin `0.10.22` 发布时依赖范围被 npm 写成 `^0.3.9`,已弃用且未进入 `workbuddy-stable`;`0.10.23` 恢复固定依赖 `0.3.9`。
47
+
13
48
  ## [0.3.8 / WorkBuddy Plugin 0.10.21 / SkillHub 1.0.5] - 2026-08-25
14
49
 
15
50
  ### 规划模式卡住保护
package/README.md CHANGED
@@ -76,7 +76,15 @@ xq-cli export --cid <cid> --out D:\output
76
76
 
77
77
  Fresh installs default to the production API address `https://ai.bidfile.qianlima.com/api`. Use `--base-url` only when you need to target another API endpoint.
78
78
 
79
- `login` now defaults to browser authorization. If you explicitly pass `--name` and `--password`, the CLI falls back to password login. If you pass `--api-key`, it saves the API key locally and uses it for later requests.
79
+ `login` now defaults to a local page with browser and API Key choices. If you explicitly pass `--name` and `--password`, the CLI uses password login. If you pass `--api-key`, it saves the API key locally and uses it for later requests.
80
+
81
+ When no credential flags are supplied, `xq-cli login` opens a local login page on
82
+ `127.0.0.1` with both choices: browser authorization or an API Key from the
83
+ Xique API Key management page. The page submits the Key only to the local CLI,
84
+ which verifies it with `/user/info` before saving it to the user-scoped config.
85
+ Use `xq-cli login --browser` to skip the local page and open browser
86
+ authorization directly, or `xq-cli login --no-open` to copy the local page URL
87
+ manually. Never paste an API Key into chat or commit it to a repository.
80
88
 
81
89
  `directory view` reads the complete generated directory tree without triggering outline, directory, or content generation. Add `--json` for the raw API structure, or `--out <file-or-directory>` to save a UTF-8 JSON document.
82
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xqyz/xq-cli",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "CLI for xique bid-book task workflows",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/cli.mjs CHANGED
@@ -441,7 +441,7 @@ function printHelp() {
441
441
  xq-cli: xique brand task CLI
442
442
 
443
443
  Commands
444
- login Save xique API key or account token locally
444
+ login Open a local login page or save xique credentials locally
445
445
  init Upload files and initialize a task
446
446
  parse-status Query or wait for parse completion
447
447
  period-status Query engineering project period confirmation status
@@ -472,6 +472,7 @@ Plan actions
472
472
 
473
473
  Examples
474
474
  xq-cli login
475
+ xq-cli login --no-open
475
476
  xq-cli login --name your-account --password your-password
476
477
  xq-cli login --api-key xq_sk_xxx
477
478
  xq-cli login --browser
@@ -517,8 +518,9 @@ Common options
517
518
  --timeout-sec <n> Poll timeout in seconds, default 600
518
519
  --interval-sec <n> Poll interval in seconds, default 10
519
520
  --reference-type <0|1|2> Outline reference choice: 0=merge, 1=format only, 2=score only
520
- --browser Force browser authorization login; browser is the default when credentials are not provided
521
+ --browser Skip the local login page and open browser authorization directly
521
522
  --api-key <key> Save a user API key locally and verify it with /user/info
523
+ --no-open Do not open the local login page automatically
522
524
  --interactive Open prompt menus for the current command
523
525
  --dry-run Preview an outline update without saving it
524
526
  --yes Confirm an outline update without an interactive prompt
@@ -610,33 +612,42 @@ async function handleLogin(args, state) {
610
612
  }, [
611
613
  `login success: ${state.user.username}`,
612
614
  'mode: api-key',
615
+ 'authType: api-key',
613
616
  `baseUrl: ${baseUrl}`,
614
617
  `config: ${CONFIG_FILE}`,
615
618
  ]);
616
619
  return;
617
620
  }
618
621
  const loginMode = resolveLoginMode(args);
619
- const result = loginMode === 'browser'
620
- ? await loginWithBrowser(baseUrl, sourceFrom, args)
621
- : await loginWithPassword(baseUrl, sourceFrom, args);
622
+ const loginResult = loginMode === 'browser'
623
+ ? { result: await loginWithBrowser(baseUrl, sourceFrom, args), authType: 'token' }
624
+ : loginMode === 'page'
625
+ ? await loginWithLoginPage(baseUrl, sourceFrom, args)
626
+ : { result: await loginWithPassword(baseUrl, sourceFrom, args), authType: 'token' };
627
+ const result = loginResult.result;
628
+ const apiKeyFromPage = loginResult.apiKey || '';
622
629
 
623
630
  saveLoginState(state, {
624
631
  baseUrl,
625
- name: stringOption(args, 'name', result.data?.user?.username || ''),
632
+ name: stringOption(args, 'name', result.data?.user?.username || result.data?.username || ''),
626
633
  sourceFrom,
627
634
  result,
635
+ apiKey: apiKeyFromPage,
636
+ authType: loginResult.authType,
628
637
  });
629
638
 
630
639
  outputResult(args, {
631
640
  command: 'login',
632
641
  mode: loginMode,
633
- authType: loginMode === 'browser' ? 'token' : 'token',
642
+ authType: loginResult.authType,
634
643
  baseUrl,
635
644
  username: state.user.username,
636
- tokenSaved: true,
645
+ tokenSaved: loginResult.authType === 'token',
646
+ apiKeySaved: loginResult.authType === 'api-key',
637
647
  }, [
638
648
  `login success: ${state.user.username}`,
639
649
  `mode: ${loginMode}`,
650
+ `authType: ${loginResult.authType}`,
640
651
  `baseUrl: ${baseUrl}`,
641
652
  `config: ${CONFIG_FILE}`,
642
653
  ]);
@@ -651,7 +662,7 @@ function resolveLoginMode(args) {
651
662
  }
652
663
  const hasName = stringOption(args, 'name');
653
664
  const hasPassword = stringOption(args, 'password');
654
- return hasName || hasPassword ? 'password' : 'browser';
665
+ return hasName || hasPassword ? 'password' : 'page';
655
666
  }
656
667
 
657
668
  async function loginWithApiKey(baseUrl, apiKey) {
@@ -702,6 +713,53 @@ async function loginWithBrowser(baseUrl, sourceFrom, args) {
702
713
  }
703
714
  }
704
715
 
716
+ async function loginWithLoginPage(baseUrl, sourceFrom, args) {
717
+ const timeoutMs = secondsToMs(numberOption(args, 'timeoutSec', DEFAULT_LOGIN_TIMEOUT_MS / 1000));
718
+ const loginState = randomUUID();
719
+ const server = await startBrowserLoginServer({
720
+ loginState,
721
+ onApiKey: async apiKey => ({
722
+ result: await loginWithApiKey(baseUrl, apiKey),
723
+ apiKey,
724
+ }),
725
+ renderPage: ({ redirectUri }) => renderLoginPage({
726
+ authorizeUrl: buildBrowserAuthorizeUrl(baseUrl, redirectUri, loginState, sourceFrom),
727
+ }),
728
+ });
729
+ writeNotice(args, `login page url: ${server.pageUrl}`);
730
+ if (process.env.XQ_CLI_NO_BROWSER !== '1') {
731
+ if (openBrowser(server.pageUrl)) {
732
+ writeNotice(args, 'login page: waiting for API key or browser authorization');
733
+ } else {
734
+ writeNotice(args, 'login page: automatic open failed, paste the URL above into a browser on this machine');
735
+ }
736
+ }
737
+
738
+ try {
739
+ const selected = await withTimeout(
740
+ server.result,
741
+ timeoutMs,
742
+ `Login timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
743
+ );
744
+ if (selected.mode === 'api-key') {
745
+ return {
746
+ result: selected.result,
747
+ apiKey: selected.apiKey,
748
+ authType: 'api-key',
749
+ };
750
+ }
751
+ return {
752
+ result: await postJson(createClient(baseUrl), '/user/cli/token', {
753
+ code: selected.code,
754
+ state: selected.state,
755
+ }),
756
+ authType: 'token',
757
+ };
758
+ } finally {
759
+ await server.close();
760
+ }
761
+ }
762
+
705
763
  function writeNotice(args, message) {
706
764
  if (booleanOption(args, 'json')) {
707
765
  console.error(message);
@@ -2153,11 +2211,20 @@ async function verifyParserWakeUp(client, state, cid, uuid, params, planningMode
2153
2211
  choiceMultiBiddingId: selectedMultiBidId,
2154
2212
  }));
2155
2213
 
2214
+ const parseAdvanced = [1, 3, 4].includes(parseSuccess);
2156
2215
  const selectionRequired = expectedMultiBidId && expectedMultiBidId !== 'notMultiBid';
2157
2216
  const selectionMatches = !selectionRequired || selectedMultiBidId === expectedMultiBidId;
2158
- const parseAdvanced = [1, 3, 4].includes(parseSuccess);
2159
- const planModeMatches = !planningMode || savedPlanMode === 1;
2160
- if (parseAdvanced && selectionMatches && planModeMatches) {
2217
+ const selectionMetadataMissing = selectionRequired && !selectedMultiBidId;
2218
+ // Planning mode persists the full configuration again when
2219
+ // `plan confirm-outline` is called. Some backend deployments complete
2220
+ // intelligent analysis (success=3) without mirroring planMode or the
2221
+ // selected package onto getTaskDetail. Treat the parse result as the
2222
+ // authority here so WorkBuddy can trigger the explicit outline
2223
+ // confirmation instead of failing before that call.
2224
+ // The same response shape occurs in quick mode: an advanced parse
2225
+ // with an omitted package field is still ahead of the success=2
2226
+ // selection gate. Keep rejecting an explicitly different package.
2227
+ if (parseAdvanced && (planningMode || selectionMatches || selectionMetadataMissing)) {
2161
2228
  return latest;
2162
2229
  }
2163
2230
 
@@ -4111,7 +4178,7 @@ function buildBrowserAuthorizeUrl(baseUrl, redirectUri, state, sourceFrom) {
4111
4178
  return `${baseUrl}/user/cli/authorize?${query.toString()}`;
4112
4179
  }
4113
4180
 
4114
- async function startBrowserLoginServer() {
4181
+ async function startBrowserLoginServer({ loginState = '', renderPage = null, onApiKey = null } = {}) {
4115
4182
  let settled = false;
4116
4183
  let resolveResult;
4117
4184
  let rejectResult;
@@ -4119,7 +4186,7 @@ async function startBrowserLoginServer() {
4119
4186
  resolveResult = resolve;
4120
4187
  rejectResult = reject;
4121
4188
  });
4122
- const server = http.createServer((request, response) => {
4189
+ const server = http.createServer(async (request, response) => {
4123
4190
  const origin = server.address();
4124
4191
  const requestUrl = new URL(request.url || '/', `http://127.0.0.1:${origin?.port || 0}`);
4125
4192
  const pathname = requestUrl.pathname;
@@ -4129,6 +4196,33 @@ async function startBrowserLoginServer() {
4129
4196
  response.end();
4130
4197
  return;
4131
4198
  }
4199
+ if (pathname === '/' && typeof renderPage === 'function') {
4200
+ response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
4201
+ response.end(renderPage({
4202
+ redirectUri: `http://127.0.0.1:${origin?.port || 0}${BROWSER_LOGIN_CALLBACK_PATH}`,
4203
+ }));
4204
+ return;
4205
+ }
4206
+ if (pathname === '/api-key' && request.method === 'POST' && typeof onApiKey === 'function') {
4207
+ try {
4208
+ const body = await readJsonRequestBody(request, 16 * 1024);
4209
+ const apiKey = normalizeApiKey(body?.apiKey);
4210
+ if (!apiKey) {
4211
+ throw new Error('请输入 API Key。');
4212
+ }
4213
+ const selected = await onApiKey(apiKey);
4214
+ response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
4215
+ response.end(renderBrowserLoginResultPage('登录成功', 'API Key 已验证,CLI 正在保存登录状态;现在可以关闭此页面。'));
4216
+ if (!settled) {
4217
+ settled = true;
4218
+ resolveResult({ mode: 'api-key', ...selected });
4219
+ }
4220
+ } catch (error) {
4221
+ response.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
4222
+ response.end(renderBrowserLoginResultPage('登录未完成', error.message || 'API Key 验证失败,请检查后重试。'));
4223
+ }
4224
+ return;
4225
+ }
4132
4226
  if (pathname !== BROWSER_LOGIN_CALLBACK_PATH) {
4133
4227
  response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
4134
4228
  response.end('Not found');
@@ -4147,11 +4241,20 @@ async function startBrowserLoginServer() {
4147
4241
  return;
4148
4242
  }
4149
4243
 
4244
+ if (loginState && state !== loginState) {
4245
+ response.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
4246
+ response.end(renderBrowserLoginResultPage('登录未完成', '授权状态校验失败,请返回 CLI 重新执行登录。'));
4247
+ if (!settled) {
4248
+ settled = true;
4249
+ rejectResult(new Error('Browser login state validation failed.'));
4250
+ }
4251
+ return;
4252
+ }
4150
4253
  response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
4151
4254
  response.end(renderBrowserLoginResultPage('登录成功', '授权已完成,现在可以回到 CLI 继续使用了。'));
4152
4255
  if (!settled) {
4153
4256
  settled = true;
4154
- resolveResult({ code, state });
4257
+ resolveResult({ mode: 'browser', code, state });
4155
4258
  }
4156
4259
  });
4157
4260
  server.on('error', error => {
@@ -4173,11 +4276,111 @@ async function startBrowserLoginServer() {
4173
4276
  const redirectUri = `http://127.0.0.1:${address.port}${BROWSER_LOGIN_CALLBACK_PATH}`;
4174
4277
  return {
4175
4278
  redirectUri,
4279
+ pageUrl: `http://127.0.0.1:${address.port}/`,
4280
+ loginState,
4176
4281
  result,
4177
4282
  close: () => closeServer(server),
4178
4283
  };
4179
4284
  }
4180
4285
 
4286
+ async function readJsonRequestBody(request, maxBytes) {
4287
+ const chunks = [];
4288
+ let total = 0;
4289
+ for await (const chunk of request) {
4290
+ total += chunk.length;
4291
+ if (total > maxBytes) {
4292
+ throw new Error('请求内容过大。');
4293
+ }
4294
+ chunks.push(chunk);
4295
+ }
4296
+ const text = Buffer.concat(chunks).toString('utf8');
4297
+ try {
4298
+ if (String(request.headers['content-type'] || '').includes('application/x-www-form-urlencoded')) {
4299
+ return Object.fromEntries(new URLSearchParams(text));
4300
+ }
4301
+ return JSON.parse(text || '{}');
4302
+ } catch {
4303
+ throw new Error('请求格式无效。');
4304
+ }
4305
+ }
4306
+
4307
+ function renderLoginPage({ authorizeUrl }) {
4308
+ return `<!doctype html>
4309
+ <html lang="zh-CN">
4310
+ <head>
4311
+ <meta charset="UTF-8" />
4312
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
4313
+ <title>喜鹊标书登录</title>
4314
+ <style>
4315
+ :root{font-family:Inter,Segoe UI,Microsoft YaHei,sans-serif;color:#182238;background:#eef3fb;}
4316
+ *{box-sizing:border-box;} body{min-height:100vh;margin:0;padding:32px;display:grid;place-items:center;background:#eef3fb;}
4317
+ .page-shell{width:min(100%,1080px);display:grid;grid-template-columns:minmax(0,1fr) 448px;gap:24px;align-items:stretch;}
4318
+ .brand-panel{position:relative;overflow:hidden;display:flex;min-height:584px;flex-direction:column;justify-content:space-between;padding:42px 44px;border:1px solid #dce5f4;border-radius:24px;background:linear-gradient(145deg,#f8fbff 0%,#eef4ff 58%,#e9f0fc 100%);box-shadow:0 20px 55px rgba(35,58,105,.10);}
4319
+ .brand-panel:after{content:"";position:absolute;width:310px;height:310px;right:-128px;bottom:-142px;border:1px solid rgba(83,111,179,.20);border-radius:50%;box-shadow:0 0 0 26px rgba(83,111,179,.05),0 0 0 52px rgba(83,111,179,.04);}
4320
+ .brand-top,.brand-footer{position:relative;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:12px;}
4321
+ .brand-kicker{display:flex;align-items:center;gap:10px;color:#536da9;font-size:12px;font-weight:800;letter-spacing:.18em;}
4322
+ .brand-symbol{display:grid;width:30px;height:30px;place-items:center;border-radius:10px;color:#fff;background:#536da9;box-shadow:0 7px 16px rgba(83,109,169,.25);font-size:18px;letter-spacing:0;}
4323
+ .brand-status{display:flex;align-items:center;gap:7px;color:#6b7b99;font-size:12px;}
4324
+ .brand-status i{width:7px;height:7px;border-radius:50%;background:#35a779;box-shadow:0 0 0 4px rgba(53,167,121,.13);}
4325
+ .brand-copy{position:relative;z-index:1;margin:auto 0;max-width:470px;}
4326
+ .brand-copy h1{margin:0;color:#14213b;font-size:clamp(38px,5vw,62px);font-weight:800;letter-spacing:-.055em;line-height:1.08;}
4327
+ .brand-copy h1 em{font-style:normal;color:#536da9;}
4328
+ .brand-copy p{max-width:400px;margin:24px 0 0;color:#637593;font-size:16px;line-height:1.85;}
4329
+ .brand-features{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:34px 0 0;}
4330
+ .brand-feature{padding:14px 13px;border:1px solid rgba(188,204,231,.72);border-radius:12px;background:rgba(255,255,255,.58);}
4331
+ .brand-feature strong,.brand-feature span{display:block;}.brand-feature strong{color:#324a7e;font-size:13px;}.brand-feature span{margin-top:5px;color:#7a89a3;font-size:11px;line-height:1.45;}
4332
+ .brand-footer{justify-content:flex-start;color:#8290a8;font-size:11px;}
4333
+ .card{width:100%;min-height:584px;padding:42px 40px;border:1px solid #e0e7f2;border-radius:24px;background:#fff;box-shadow:0 20px 55px rgba(35,58,105,.13);}
4334
+ .card-heading{display:flex;align-items:center;gap:11px;margin-bottom:24px;}.card-heading-mark{display:grid;width:38px;height:38px;place-items:center;border:1px solid #dce4f4;border-radius:12px;color:#536da9;background:#f2f6ff;font-size:18px;}.card-heading small{display:block;color:#7a89a3;font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;}.card-heading h2{margin:3px 0 0;color:#182238;font-size:25px;letter-spacing:-.03em;}
4335
+ .muted{margin:0 0 28px;color:#6e7e99;line-height:1.75;font-size:14px;}
4336
+ .field-label{display:flex;align-items:center;justify-content:space-between;margin:0 0 9px;color:#2c3a55;font-size:13px;font-weight:700;}.field-label span{color:#a4afc0;font-size:11px;font-weight:500;}
4337
+ .input-wrap{position:relative;}.field-icon{position:absolute;top:50%;left:15px;width:18px;height:18px;transform:translateY(-50%);}.field-icon:before{content:"";position:absolute;top:1px;left:5px;width:7px;height:7px;border:1.5px solid #7183a5;border-radius:50%;}.field-icon:after{content:"";position:absolute;top:8px;left:8px;width:1.5px;height:9px;background:#7183a5;box-shadow:3px 5px 0 -0.25px #7183a5;}
4338
+ input{display:block;width:100%;height:50px;padding:0 14px 0 47px;border:1px solid #d4deec;border-radius:12px;outline:0;color:#1c2b46;background:#fbfcfe;font:inherit;font-size:14px;transition:border-color .18s,box-shadow .18s,background .18s;} input::placeholder{color:#aeb9c9;} input:focus{border-color:#6d86bc;background:#fff;box-shadow:0 0 0 4px rgba(83,109,169,.12);}
4339
+ button,a{display:flex;width:100%;min-height:50px;box-sizing:border-box;align-items:center;justify-content:center;gap:8px;padding:12px 14px;border-radius:12px;font:inherit;font-size:14px;font-weight:700;text-align:center;text-decoration:none;cursor:pointer;transition:transform .18s,box-shadow .18s,border-color .18s,background .18s;}
4340
+ button{margin-top:13px;border:0;background:#536da9;color:#fff;box-shadow:0 10px 22px rgba(83,109,169,.23);} button:hover{background:#47619b;box-shadow:0 13px 26px rgba(83,109,169,.28);transform:translateY(-1px);} button:disabled{cursor:wait;opacity:.72;transform:none;}
4341
+ .button-arrow{font-size:18px;line-height:1;}.divider{display:flex;align-items:center;gap:12px;color:#a1aec2;margin:23px 0 13px;font-size:12px;}.divider:before,.divider:after{content:"";height:1px;background:#e8edf4;flex:1;}
4342
+ a{border:1px solid #d5dfed;color:#304364;background:#fff;} a:hover{border-color:#91a6cc;background:#f7f9fd;transform:translateY(-1px);}
4343
+ .hint{margin:16px 0 0;color:#8390a6;font-size:12px;line-height:1.7;}.hint strong{color:#536da9;font-weight:700;}.secure-note{display:flex;align-items:center;gap:8px;margin-top:30px;padding-top:17px;border-top:1px solid #edf0f5;color:#9aa6b7;font-size:11px;}.secure-note span{width:7px;height:7px;border-radius:50%;background:#35a779;box-shadow:0 0 0 4px rgba(53,167,121,.10);}
4344
+ @media (max-width:820px){body{padding:18px;}.page-shell{grid-template-columns:1fr;max-width:520px;}.brand-panel{display:none;}.card{min-height:0;padding:32px 26px;border-radius:20px;}}
4345
+ @media (max-width:420px){body{padding:12px;}.card{padding:27px 20px;}.card-heading h2{font-size:22px;}}
4346
+ </style>
4347
+ </head>
4348
+ <body>
4349
+ <div class="page-shell">
4350
+ <section class="brand-panel" aria-label="喜鹊标书 CLI">
4351
+ <div class="brand-top">
4352
+ <div class="brand-kicker"><span class="brand-symbol">↗</span> XIQUE CLI</div>
4353
+ <div class="brand-status"><i></i> 本机安全接入</div>
4354
+ </div>
4355
+ <div class="brand-copy">
4356
+ <h1>把喜鹊标书<br /><em>带到终端</em></h1>
4357
+ <p>一次登录,连接初始化、大纲、正文与 Word 导出。把重复操作交给 CLI,把精力留给标书本身。</p>
4358
+ <div class="brand-features">
4359
+ <div class="brand-feature"><strong>一次登录</strong><span>后续命令自动复用</span></div>
4360
+ <div class="brand-feature"><strong>分阶段工作流</strong><span>从大纲到正文可控推进</span></div>
4361
+ <div class="brand-feature"><strong>随时可轮换</strong><span>账号中心独立管理 Key</span></div>
4362
+ </div>
4363
+ </div>
4364
+ <div class="brand-footer">喜鹊标书 AI · 命令行工作台</div>
4365
+ </section>
4366
+ <main class="card">
4367
+ <div class="card-heading"><span class="card-heading-mark">⌁</span><div><small>LOCAL ACCESS</small><h2>登录喜鹊标书</h2></div></div>
4368
+ <p class="muted">使用 API Key 登录后,xq-cli 会在本机保存凭证,后续命令无需重复输入。</p>
4369
+ <form method="post" action="/api-key" onsubmit="this.querySelector('button').disabled=true">
4370
+ <label class="field-label" for="api-key">API Key <span>xq_sk_ 开头</span></label>
4371
+ <div class="input-wrap"><span class="field-icon" aria-hidden="true"></span><input id="api-key" name="apiKey" type="password" autocomplete="off" placeholder="xq_sk_..." required /></div>
4372
+ <button type="submit"><span>使用 API Key 登录</span><span class="button-arrow">→</span></button>
4373
+ </form>
4374
+ <div class="divider">或</div>
4375
+ <a href="${escapeHtml(authorizeUrl)}"><span>使用浏览器授权登录</span><span class="button-arrow">↗</span></a>
4376
+ <p class="hint">API Key 可在喜鹊的 API Key 管理页创建。验证成功后即可关闭页面。</p>
4377
+ <div class="secure-note"><span></span>登录状态仅用于当前 CLI 工作流</div>
4378
+ </main>
4379
+ </div>
4380
+ </body>
4381
+ </html>`;
4382
+ }
4383
+
4181
4384
  function renderBrowserLoginResultPage(title, message) {
4182
4385
  return `<!doctype html>
4183
4386
  <html lang="zh-CN">