@xqyz/xq-cli 0.3.9 → 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 +27 -1
- package/README.md +9 -1
- package/package.json +1 -1
- package/src/cli.mjs +211 -13
package/CHANGELOG.md
CHANGED
|
@@ -10,7 +10,32 @@
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
-
|
|
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
|
|
14
39
|
|
|
15
40
|
### WorkBuddy 规划模式自动推进
|
|
16
41
|
|
|
@@ -18,6 +43,7 @@
|
|
|
18
43
|
- WorkBuddy 检测到 `analysis_ready` 后自动对同一 `cid` 执行 `plan confirm-outline`,不再展示 `analysis-review` 或智能解读确认卡。
|
|
19
44
|
- 已被旧版本误标失败、但错误中已记录解析 `success=3` 的规划任务,在查询同一 `runId` 时会原地恢复并自动确认生成大纲。
|
|
20
45
|
- 保留大纲和目录人工确认闸门;自动推进只覆盖后台智能解读到大纲生成这一段,不会提前生成正文。
|
|
46
|
+
- WorkBuddy Plugin `0.10.22` 发布时依赖范围被 npm 写成 `^0.3.9`,已弃用且未进入 `workbuddy-stable`;`0.10.23` 恢复固定依赖 `0.3.9`。
|
|
21
47
|
|
|
22
48
|
## [0.3.8 / WorkBuddy Plugin 0.10.21 / SkillHub 1.0.5] - 2026-08-25
|
|
23
49
|
|
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
|
|
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
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
|
|
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
|
|
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
|
|
620
|
-
? await loginWithBrowser(baseUrl, sourceFrom, args)
|
|
621
|
-
:
|
|
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:
|
|
642
|
+
authType: loginResult.authType,
|
|
634
643
|
baseUrl,
|
|
635
644
|
username: state.user.username,
|
|
636
|
-
tokenSaved:
|
|
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' : '
|
|
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);
|
|
@@ -2156,13 +2214,17 @@ async function verifyParserWakeUp(client, state, cid, uuid, params, planningMode
|
|
|
2156
2214
|
const parseAdvanced = [1, 3, 4].includes(parseSuccess);
|
|
2157
2215
|
const selectionRequired = expectedMultiBidId && expectedMultiBidId !== 'notMultiBid';
|
|
2158
2216
|
const selectionMatches = !selectionRequired || selectedMultiBidId === expectedMultiBidId;
|
|
2217
|
+
const selectionMetadataMissing = selectionRequired && !selectedMultiBidId;
|
|
2159
2218
|
// Planning mode persists the full configuration again when
|
|
2160
2219
|
// `plan confirm-outline` is called. Some backend deployments complete
|
|
2161
2220
|
// intelligent analysis (success=3) without mirroring planMode or the
|
|
2162
2221
|
// selected package onto getTaskDetail. Treat the parse result as the
|
|
2163
2222
|
// authority here so WorkBuddy can trigger the explicit outline
|
|
2164
2223
|
// confirmation instead of failing before that call.
|
|
2165
|
-
|
|
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)) {
|
|
2166
2228
|
return latest;
|
|
2167
2229
|
}
|
|
2168
2230
|
|
|
@@ -4116,7 +4178,7 @@ function buildBrowserAuthorizeUrl(baseUrl, redirectUri, state, sourceFrom) {
|
|
|
4116
4178
|
return `${baseUrl}/user/cli/authorize?${query.toString()}`;
|
|
4117
4179
|
}
|
|
4118
4180
|
|
|
4119
|
-
async function startBrowserLoginServer() {
|
|
4181
|
+
async function startBrowserLoginServer({ loginState = '', renderPage = null, onApiKey = null } = {}) {
|
|
4120
4182
|
let settled = false;
|
|
4121
4183
|
let resolveResult;
|
|
4122
4184
|
let rejectResult;
|
|
@@ -4124,7 +4186,7 @@ async function startBrowserLoginServer() {
|
|
|
4124
4186
|
resolveResult = resolve;
|
|
4125
4187
|
rejectResult = reject;
|
|
4126
4188
|
});
|
|
4127
|
-
const server = http.createServer((request, response) => {
|
|
4189
|
+
const server = http.createServer(async (request, response) => {
|
|
4128
4190
|
const origin = server.address();
|
|
4129
4191
|
const requestUrl = new URL(request.url || '/', `http://127.0.0.1:${origin?.port || 0}`);
|
|
4130
4192
|
const pathname = requestUrl.pathname;
|
|
@@ -4134,6 +4196,33 @@ async function startBrowserLoginServer() {
|
|
|
4134
4196
|
response.end();
|
|
4135
4197
|
return;
|
|
4136
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
|
+
}
|
|
4137
4226
|
if (pathname !== BROWSER_LOGIN_CALLBACK_PATH) {
|
|
4138
4227
|
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
4139
4228
|
response.end('Not found');
|
|
@@ -4152,11 +4241,20 @@ async function startBrowserLoginServer() {
|
|
|
4152
4241
|
return;
|
|
4153
4242
|
}
|
|
4154
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
|
+
}
|
|
4155
4253
|
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
4156
4254
|
response.end(renderBrowserLoginResultPage('登录成功', '授权已完成,现在可以回到 CLI 继续使用了。'));
|
|
4157
4255
|
if (!settled) {
|
|
4158
4256
|
settled = true;
|
|
4159
|
-
resolveResult({ code, state });
|
|
4257
|
+
resolveResult({ mode: 'browser', code, state });
|
|
4160
4258
|
}
|
|
4161
4259
|
});
|
|
4162
4260
|
server.on('error', error => {
|
|
@@ -4178,11 +4276,111 @@ async function startBrowserLoginServer() {
|
|
|
4178
4276
|
const redirectUri = `http://127.0.0.1:${address.port}${BROWSER_LOGIN_CALLBACK_PATH}`;
|
|
4179
4277
|
return {
|
|
4180
4278
|
redirectUri,
|
|
4279
|
+
pageUrl: `http://127.0.0.1:${address.port}/`,
|
|
4280
|
+
loginState,
|
|
4181
4281
|
result,
|
|
4182
4282
|
close: () => closeServer(server),
|
|
4183
4283
|
};
|
|
4184
4284
|
}
|
|
4185
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
|
+
|
|
4186
4384
|
function renderBrowserLoginResultPage(title, message) {
|
|
4187
4385
|
return `<!doctype html>
|
|
4188
4386
|
<html lang="zh-CN">
|