@morehao/dsh-github-trending 0.8.2 → 0.8.4

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
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.8.4] - 2026-11-02
4
+
5
+ ### Features
6
+
7
+ - **auth**: open device-code page with prefilled code
8
+ - **auth**: 美化 GitHub OAuth 回调落地页 (#78)
9
+
10
+ **Full Changelog**: https://github.com/morehao/dsh-github-trending/compare/v0.8.3...v0.8.4
11
+
12
+ ## [v0.8.3] - 2026-11-02
13
+
14
+ ### Fixes
15
+
16
+ - **auth**: fall back to device flow when client_secret is missing (#75)
17
+
18
+ **Full Changelog**: https://github.com/morehao/dsh-github-trending/compare/v0.8.2...v0.8.3
19
+
3
20
  ## [v0.8.2] - 2026-11-02
4
21
 
5
22
  ### Fixes
package/README.md CHANGED
@@ -59,13 +59,13 @@ GitHub 趋势榜的 **DeepSeek Harness 插件**:按日 / 周 / 月周期定时
59
59
 
60
60
  认证分区支持三种方式(可随时切换):
61
61
 
62
- - **浏览器登录(默认推荐)**:点「用 GitHub 登录」在浏览器完成授权,无需手工粘贴 token。走 OAuth PKCE + 本机回环回调(或设备码),token 由 DSH 凭据记录安全保存,不落 `settings.json`。插件内置了公开的 OAuth App client_id(client_id 属公开标识、非机密;纯 PKCE 不使用 client_secret),开箱即用。
62
+ - **浏览器登录(默认推荐)**:点「用 GitHub 登录」在浏览器完成授权,无需手工粘贴 token,token 由 DSH 凭据记录安全保存、不落 `settings.json`。插件内置了公开的 OAuth App client_id,开箱即用;GitHub 的 web flow 换 token 仍然**必须带 client_secret**(PKCE 只是叠加保护),而客户端不能内置机密,所以默认走**设备码**:点一下后在打开的 GitHub 页面输入验证码即可。若你自带 OAuth App,可在插件配置里同时给 `githubClientId` + `githubClientSecret`,就会改用本机回环回调(不用手输验证码)。
63
63
  - **本机 gh**:复用你终端里 `gh auth login` 的登录态。
64
64
  - **PAT**:手工填 Personal Access Token(存到凭据引用 / 环境变量)。
65
65
 
66
66
  > 浏览器登录需要核心提供 `authorization` 服务,并且 GitHub flow 注册成功才算可用;两者任一不满足时,设置页直接提示不可用并引导改用 gh / PAT,而不是给一个点了没反应的按钮。宿主启动时服务可能晚于插件就绪,插件会等服务就绪再注册,并在读状态时再兜底一次。
67
67
  >
68
- > 授权页默认尝试自动打开;若被浏览器的弹窗拦截,状态行里始终有可点的「打开授权页」链接与「复制链接」。
68
+ > 设备码 / 授权页默认尝试自动打开;若被浏览器的弹窗拦截,状态行里始终有可点的「打开授权页」链接、验证码与「复制链接」。
69
69
 
70
70
  ### 订阅
71
71
 
package/client.js CHANGED
@@ -125,10 +125,10 @@ var DICT = {
125
125
  "auth.gh": "本机 gh",
126
126
  "auth.browser": "浏览器登录",
127
127
  "auth.gh.hint": "使用本机 gh 登录态:终端执行 `gh auth login` 完成登录,再点「测试认证」。",
128
- "auth.browser.hint": "在浏览器里完成 GitHub 授权,token 由 DSH 安全保存,无需手工粘贴。",
128
+ "auth.browser.hint": "在浏览器里完成 GitHub 授权,token 由 DSH 安全保存,无需手工粘贴。点一下会在浏览器打开授权页(验证码通常已自动填入),确认授权即可。",
129
129
  "auth.browser.signin": "用 GitHub 登录",
130
130
  "auth.browser.waiting": "等待浏览器授权…",
131
- "auth.browser.code": "在打开的页面输入验证码:",
131
+ "auth.browser.code": "如未自动填入,请手动输入验证码:",
132
132
  "auth.browser.cancel": "取消",
133
133
  "auth.browser.done": "已登录",
134
134
  "auth.browser.device": "用设备码登录",
@@ -139,7 +139,7 @@ var DICT = {
139
139
  "auth.browser.copied": "授权链接已复制",
140
140
  "auth.browser.failed": "登录失败",
141
141
  "auth.browser.failed.detail": "登录失败:{message}",
142
- "auth.browser.failed.hint": "宿主没有完成 GitHub 授权流程的注册,请重试;仍失败请改用「本机 gh」或 PAT。",
142
+ "auth.browser.failed.hint": "请重试一次;仍失败可改用「本机 gh」或 PAT。",
143
143
  "auth.pat.hint": "在 GitHub → Settings → Developer settings 生成 Personal Access Token(勾选 public_repo),填到下方引用或环境变量。",
144
144
  "auth.tokenRef": "凭据引用",
145
145
  "auth.tokenRef.hint": "凭据引用名 / 环境变量名",
@@ -670,10 +670,10 @@ var DICT = {
670
670
  "auth.gh": "Local gh",
671
671
  "auth.browser": "Browser login",
672
672
  "auth.gh.hint": "Uses your local gh login: run `gh auth login` in a terminal, then click Test authentication.",
673
- "auth.browser.hint": "Authorize GitHub in your browser; DSH stores the token securely, no manual paste.",
673
+ "auth.browser.hint": "Authorize GitHub in your browser; DSH stores the token securely, no manual paste. Clicking opens the authorization page with the code usually pre-filled — just confirm.",
674
674
  "auth.browser.signin": "Sign in with GitHub",
675
675
  "auth.browser.waiting": "Waiting for browser authorization…",
676
- "auth.browser.code": "Enter this code on the opened page:",
676
+ "auth.browser.code": "If it is not filled in automatically, enter this code:",
677
677
  "auth.browser.cancel": "Cancel",
678
678
  "auth.browser.done": "Signed in",
679
679
  "auth.browser.device": "Use a device code",
@@ -684,7 +684,7 @@ var DICT = {
684
684
  "auth.browser.copied": "Authorization link copied",
685
685
  "auth.browser.failed": "Sign-in failed",
686
686
  "auth.browser.failed.detail": "Sign-in failed: {message}",
687
- "auth.browser.failed.hint": "The host did not register the GitHub authorization flow. Retry, or use Local gh / a PAT instead.",
687
+ "auth.browser.failed.hint": "Retry once; if it keeps failing, use Local gh or a PAT instead.",
688
688
  "auth.pat.hint": "Create a Personal Access Token under GitHub → Settings → Developer settings (scope public_repo) and put it in the ref below or an env var.",
689
689
  "auth.tokenRef": "Credential ref",
690
690
  "auth.tokenRef.hint": "credential ref / env var name",
@@ -2272,6 +2272,13 @@ function SettingsView(props) {
2272
2272
  } catch {
2273
2273
  }
2274
2274
  }
2275
+ const deviceUrl = view.verificationUriComplete ?? view.verificationUri;
2276
+ if (view.phase === "waiting-device" && deviceUrl !== void 0) {
2277
+ try {
2278
+ window.open(deviceUrl, "_blank", "noopener,noreferrer");
2279
+ } catch {
2280
+ }
2281
+ }
2275
2282
  } catch (error) {
2276
2283
  notify("err", error instanceof Error ? error.message : String(error));
2277
2284
  }
@@ -2348,6 +2355,7 @@ function SettingsView(props) {
2348
2355
  const isGh = draft.github.authMode === "gh";
2349
2356
  const isBrowser = draft.github.authMode === "browser";
2350
2357
  const browserAvailable = state.github.browserAvailable === true;
2358
+ const loopbackAvailable = state.github.loopbackAvailable === true;
2351
2359
  const sections = [
2352
2360
  { id: "source", label: t("st.source") },
2353
2361
  { id: "model", label: t("st.model") },
@@ -2542,12 +2550,12 @@ function SettingsView(props) {
2542
2550
  {
2543
2551
  small: true,
2544
2552
  variant: "primary",
2545
- onClick: () => void startSignIn("browser"),
2553
+ onClick: () => void startSignIn(loopbackAvailable ? "browser" : "device"),
2546
2554
  disabled: signInActive
2547
2555
  },
2548
2556
  signIn.phase === "done" && signIn.login !== void 0 ? `${t("auth.browser.done")} · @${signIn.login}` : signInActive ? t("auth.browser.waiting") : t("auth.browser.signin")
2549
2557
  ) : h("span", { className: "dshgt-hint" }, t("auth.browser.unavailable")),
2550
- browserAvailable ? h(Btn, { small: true, variant: "ghost", onClick: () => void startSignIn("device"), disabled: signInActive }, t("auth.browser.device")) : null,
2558
+ browserAvailable && loopbackAvailable ? h(Btn, { small: true, variant: "ghost", onClick: () => void startSignIn("device"), disabled: signInActive }, t("auth.browser.device")) : null,
2551
2559
  signInActive ? h(Btn, { small: true, variant: "ghost", onClick: () => void cancelSignIn() }, t("auth.browser.cancel")) : null
2552
2560
  ) : h(
2553
2561
  Row,
@@ -2576,10 +2584,19 @@ function SettingsView(props) {
2576
2584
  " ",
2577
2585
  h(Btn, { small: true, variant: "ghost", onClick: () => copyAuthorizeLink() }, t("auth.browser.copy"))
2578
2586
  ) : null,
2579
- signIn.verificationUri !== void 0 ? h(
2587
+ signIn.verificationUriComplete !== void 0 || signIn.verificationUri !== void 0 ? h(
2580
2588
  "div",
2581
2589
  { className: "dshgt-desc" },
2582
- h("a", { className: "dshgt-link", href: signIn.verificationUri, target: "_blank", rel: "noreferrer" }, signIn.verificationUri)
2590
+ h(
2591
+ "a",
2592
+ {
2593
+ className: "dshgt-link",
2594
+ href: signIn.verificationUriComplete ?? signIn.verificationUri,
2595
+ target: "_blank",
2596
+ rel: "noreferrer"
2597
+ },
2598
+ t("auth.browser.open")
2599
+ )
2583
2600
  ) : null,
2584
2601
  signIn.userCode !== void 0 ? h("div", { className: "dshgt-mono" }, `${t("auth.browser.code")} ${signIn.userCode}`) : null
2585
2602
  ) : null,
@@ -3540,9 +3540,14 @@ __export(github_auth_exports, {
3540
3540
  createPkce: () => createPkce,
3541
3541
  ensureGithubFlow: () => ensureGithubFlow,
3542
3542
  isGithubFlowRegistered: () => isGithubFlowRegistered,
3543
+ isHttpUrl: () => isHttpUrl,
3544
+ isLoopbackSignInAvailable: () => isLoopbackSignInAvailable,
3543
3545
  registerGithubFlow: () => registerGithubFlow,
3546
+ renderCallbackPage: () => renderCallbackPage,
3544
3547
  resetAttemptForTest: () => resetAttemptForTest,
3545
3548
  resolveClientId: () => resolveClientId,
3549
+ resolveClientSecret: () => resolveClientSecret,
3550
+ resolveMethod: () => resolveMethod,
3546
3551
  verifyState: () => verifyState
3547
3552
  });
3548
3553
  import { createHash as createHash3, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
@@ -3553,6 +3558,16 @@ function resolveClientId(deps) {
3553
3558
  const fromConfig = deps.clientId;
3554
3559
  return fromConfig !== void 0 && fromConfig !== "" ? fromConfig : DEFAULT_GITHUB_CLIENT_ID;
3555
3560
  }
3561
+ function resolveClientSecret(deps) {
3562
+ const secret = deps.clientSecret;
3563
+ return secret !== void 0 && secret !== "" ? secret : void 0;
3564
+ }
3565
+ function isLoopbackSignInAvailable(deps) {
3566
+ return resolveClientSecret(deps) !== void 0;
3567
+ }
3568
+ function resolveMethod(deps, requested) {
3569
+ return requested === "browser" && !isLoopbackSignInAvailable(deps) ? "device" : requested;
3570
+ }
3556
3571
  var AUTHORIZE_URL = "https://github.com/login/oauth/authorize";
3557
3572
  var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
3558
3573
  var DEVICE_CODE_URL = "https://github.com/login/device/code";
@@ -3568,6 +3583,16 @@ function attemptView() {
3568
3583
  function base64url(buf) {
3569
3584
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
3570
3585
  }
3586
+ function isHttpUrl(value) {
3587
+ if (typeof value !== "string" || value === "") return false;
3588
+ let url;
3589
+ try {
3590
+ url = new URL(value);
3591
+ } catch {
3592
+ return false;
3593
+ }
3594
+ return url.protocol === "https:" || url.protocol === "http:";
3595
+ }
3571
3596
  function createPkce() {
3572
3597
  const verifier = base64url(randomBytes2(32));
3573
3598
  const challenge = base64url(createHash3("sha256").update(verifier).digest());
@@ -3578,6 +3603,41 @@ function verifyState(received, expected) {
3578
3603
  const b = Buffer.from(expected);
3579
3604
  return a.length === b.length && timingSafeEqual2(a, b);
3580
3605
  }
3606
+ function renderCallbackPage(kind) {
3607
+ const ok = kind === "success";
3608
+ const title = ok ? "\u767B\u5F55\u5B8C\u6210" : "\u767B\u5F55\u672A\u5B8C\u6210";
3609
+ const heading = ok ? "GitHub \u6388\u6743\u6210\u529F" : "GitHub \u6388\u6743\u5931\u8D25";
3610
+ const body = ok ? "\u5DF2\u5B8C\u6210\u767B\u5F55\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002" : "\u6388\u6743\u6821\u9A8C\u672A\u901A\u8FC7\uFF0C\u8BF7\u56DE\u5230 DSH \u91CD\u65B0\u53D1\u8D77\u767B\u5F55\u3002";
3611
+ const accent = ok ? "#1f883d" : "#cf222e";
3612
+ const icon = ok ? '<path d="M20 6 9 17l-5-5"/>' : '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>';
3613
+ const autoClose = ok ? "<script>(function(){try{setTimeout(function(){window.close()},1200)}catch(e){}})()</script>" : "";
3614
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
3615
+ <meta name="viewport" content="width=device-width,initial-scale=1">
3616
+ <meta name="color-scheme" content="light dark">
3617
+ <title>${title}</title>
3618
+ <style>
3619
+ :root{--bg:#f6f8fa;--card:#ffffff;--fg:#1f2328;--muted:#59636e;--border:#d1d9e0;--accent:${accent}}
3620
+ @media (prefers-color-scheme:dark){:root{--bg:#0d1117;--card:#151b23;--fg:#e6edf3;--muted:#9198a1;--border:#3d444d}}
3621
+ *{box-sizing:border-box}
3622
+ html,body{height:100%}
3623
+ body{margin:0;display:flex;align-items:center;justify-content:center;padding:24px;
3624
+ background:var(--bg);color:var(--fg);
3625
+ font:400 15px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif}
3626
+ main{width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);
3627
+ border-radius:14px;padding:40px 32px;text-align:center;
3628
+ box-shadow:0 8px 24px rgba(0,0,0,.06)}
3629
+ .mark{width:64px;height:64px;margin:0 auto 20px;border-radius:50%;display:flex;
3630
+ align-items:center;justify-content:center;background:color-mix(in srgb,var(--accent) 14%,transparent);color:var(--accent)}
3631
+ .mark svg{width:30px;height:30px;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
3632
+ h1{margin:0 0 8px;font-size:19px;font-weight:600;letter-spacing:.2px}
3633
+ p{margin:0;color:var(--muted);font-size:14px}
3634
+ </style></head>
3635
+ <body><main role="status">
3636
+ <div class="mark"><svg viewBox="0 0 24 24" aria-hidden="true">${icon}</svg></div>
3637
+ <h1>${heading}</h1>
3638
+ <p>${body}</p>
3639
+ </main>${autoClose}</body></html>`;
3640
+ }
3581
3641
  function buildAuthorizeUrl(params) {
3582
3642
  const url = new URL(params.authorizeEndpoint);
3583
3643
  url.searchParams.set("client_id", params.clientId);
@@ -3589,21 +3649,29 @@ function buildAuthorizeUrl(params) {
3589
3649
  return url.toString();
3590
3650
  }
3591
3651
  async function exchangeCode(fetchImpl, endpoint, params, signal) {
3652
+ const form = new URLSearchParams({
3653
+ client_id: params.clientId,
3654
+ code: params.code,
3655
+ code_verifier: params.verifier,
3656
+ redirect_uri: params.redirectUri
3657
+ });
3658
+ if (params.clientSecret !== void 0) form.set("client_secret", params.clientSecret);
3592
3659
  const response = await fetchImpl(endpoint, {
3593
3660
  method: "POST",
3594
3661
  headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" },
3595
- body: new URLSearchParams({
3596
- client_id: params.clientId,
3597
- code: params.code,
3598
- code_verifier: params.verifier,
3599
- redirect_uri: params.redirectUri
3600
- }).toString(),
3662
+ body: form.toString(),
3601
3663
  signal
3602
3664
  });
3603
3665
  if (!response.ok) throw new Error(`GitHub access_token HTTP ${response.status}`);
3604
3666
  const body = await response.json();
3605
3667
  if (typeof body.access_token !== "string" || body.access_token === "") {
3606
- throw new Error(`GitHub access_token \u672A\u8FD4\u56DE token${typeof body.error === "string" ? `\uFF08${body.error}\uFF09` : ""}`);
3668
+ const error = typeof body.error === "string" ? body.error : void 0;
3669
+ if (error === "incorrect_client_credentials") {
3670
+ throw new Error(
3671
+ params.clientSecret === void 0 ? "GitHub \u7F51\u9875\u6388\u6743\u9700\u8981 client_secret\uFF08incorrect_client_credentials\uFF09\uFF0C\u5185\u7F6E\u7684\u516C\u5F00 client_id \u65E0\u6CD5\u5B8C\u6210\u8FD9\u4E00\u6B65\uFF1A\u8BF7\u6539\u7528\u8BBE\u5907\u7801\u767B\u5F55\uFF0C\u6216\u914D\u7F6E\u81EA\u5DF1\u7684 OAuth App client_id + client_secret\u3002" : "GitHub \u8BA4\u4E3A\u8FD9\u4E2A OAuth App \u7684 client_id / client_secret \u4E0D\u6B63\u786E\uFF08incorrect_client_credentials\uFF09\uFF1A\u8BF7\u68C0\u67E5\u63D2\u4EF6\u914D\u7F6E\uFF0C\u6216\u6539\u7528\u8BBE\u5907\u7801\u767B\u5F55\u3002"
3672
+ );
3673
+ }
3674
+ throw new Error(`GitHub access_token \u672A\u8FD4\u56DE token${error === void 0 ? "" : `\uFF08${error}\uFF09`}`);
3607
3675
  }
3608
3676
  return {
3609
3677
  token: body.access_token,
@@ -3631,6 +3699,10 @@ async function runBrowser(deps, session) {
3631
3699
  }
3632
3700
  const clientId = resolveClientId(deps);
3633
3701
  if (clientId === void 0 || clientId === "") throw new Error("\u7F3A\u5C11 GitHub OAuth client_id");
3702
+ const clientSecret = resolveClientSecret(deps);
3703
+ if (clientSecret === void 0) {
3704
+ throw new Error("\u56DE\u73AF\u767B\u5F55\u4E0D\u53EF\u7528\uFF1A\u6CA1\u6709\u914D\u7F6E GitHub OAuth App \u7684 client_secret\uFF0C\u8BF7\u6539\u7528\u8BBE\u5907\u7801\u767B\u5F55");
3705
+ }
3634
3706
  const fetchImpl = deps.fetchImpl ?? fetch;
3635
3707
  const endpoints = normalizeEndpoints(deps.endpoints);
3636
3708
  const scope = deps.scope ?? DEFAULT_SCOPE;
@@ -3659,10 +3731,10 @@ async function runBrowser(deps, session) {
3659
3731
  const single = url.searchParams.getAll("state").length === 1 && url.searchParams.getAll("code").length === 1;
3660
3732
  const bad = req.method !== "GET" || url.pathname !== CALLBACK_PATH || !single || code === "" || !verifyState(gotState, state);
3661
3733
  if (bad) {
3662
- res.writeHead(400, { "cache-control": "no-store" }).end();
3734
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(renderCallbackPage("error"));
3663
3735
  return;
3664
3736
  }
3665
- res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end('<!doctype html><meta charset="utf-8"><title>GitHub</title><p>\u5DF2\u5B8C\u6210\u767B\u5F55\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002</p>');
3737
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(renderCallbackPage("success"));
3666
3738
  resolveCode(code);
3667
3739
  }
3668
3740
  });
@@ -3681,7 +3753,12 @@ async function runBrowser(deps, session) {
3681
3753
  session.notify?.({ message: "Continue in your browser to sign in to GitHub.", url: authorizeUrl });
3682
3754
  const code = await withAbort(codePromise, session.signal, () => rejectCode(new Error("authorization cancelled")));
3683
3755
  updateCurrent({ phase: "exchanging" });
3684
- const exchanged = await exchangeCode(fetchImpl, endpoints.accessToken, { clientId, code, verifier, redirectUri }, session.signal);
3756
+ const exchanged = await exchangeCode(
3757
+ fetchImpl,
3758
+ endpoints.accessToken,
3759
+ { clientId, clientSecret, code, verifier, redirectUri },
3760
+ session.signal
3761
+ );
3685
3762
  const login = await fetchLogin(fetchImpl, endpoints.user, exchanged.token, session.signal);
3686
3763
  const payload = {
3687
3764
  version: 1,
@@ -3711,21 +3788,26 @@ async function runDevice(deps, session) {
3711
3788
  if (!startResponse.ok) throw new Error(`GitHub device/code HTTP ${startResponse.status}`);
3712
3789
  const start = await startResponse.json();
3713
3790
  if (typeof start.device_code !== "string" || typeof start.user_code !== "string" || typeof start.verification_uri !== "string") {
3714
- throw new Error("GitHub device/code \u8FD4\u56DE\u4E0D\u5B8C\u6574");
3791
+ const error = typeof start.error === "string" ? start.error : void 0;
3792
+ throw new Error(
3793
+ error === "device_flow_disabled" ? "GitHub \u6CA1\u6709\u4E3A\u8FD9\u4E2A OAuth App \u542F\u7528\u8BBE\u5907\u7801\u767B\u5F55\uFF1A\u8BF7\u5230 OAuth App \u8BBE\u7F6E\u52FE\u9009 Device flow\uFF0C\u6216\u6539\u7528\u300C\u672C\u673A gh\u300D/ PAT\u3002" : `GitHub device/code \u8FD4\u56DE\u4E0D\u5B8C\u6574${error === void 0 ? "" : `\uFF08${error}\uFF09`}`
3794
+ );
3715
3795
  }
3716
3796
  const deviceCode = start.device_code;
3717
3797
  const intervalSec = typeof start.interval === "number" && start.interval > 0 ? start.interval : 5;
3718
3798
  const expiresInSec = typeof start.expires_in === "number" && start.expires_in > 0 ? start.expires_in : 900;
3719
3799
  const deadline = Date.now() + expiresInSec * 1e3;
3800
+ const verificationUriComplete = isHttpUrl(start.verification_uri_complete) ? start.verification_uri_complete : void 0;
3720
3801
  updateCurrent({
3721
3802
  phase: "waiting-device",
3722
3803
  verificationUri: start.verification_uri,
3804
+ ...verificationUriComplete === void 0 ? {} : { verificationUriComplete },
3723
3805
  userCode: start.user_code,
3724
3806
  expiresAt: deadline
3725
3807
  });
3726
3808
  session.notify?.({
3727
3809
  message: "Enter this code on the verification page to finish signing in.",
3728
- url: start.verification_uri,
3810
+ url: verificationUriComplete ?? start.verification_uri,
3729
3811
  code: start.user_code
3730
3812
  });
3731
3813
  let waitMs = intervalSec * 1e3;
@@ -3829,6 +3911,10 @@ async function beginGithubSignIn(deps, method = "browser") {
3829
3911
  if (current !== void 0 && (current.phase === "waiting-browser" || current.phase === "waiting-device" || current.phase === "exchanging")) {
3830
3912
  throw new Error("already-in-flight");
3831
3913
  }
3914
+ const effectiveMethod = resolveMethod(deps, method);
3915
+ if (effectiveMethod !== method) {
3916
+ deps.logger?.("auth.method.fallback", { from: method, to: effectiveMethod, reason: "no-client-secret" });
3917
+ }
3832
3918
  const controller = new AbortController();
3833
3919
  const timer = setTimeout(() => controller.abort(), attemptTimeout(deps));
3834
3920
  const attempt = { phase: "idle", controller, done: Promise.resolve() };
@@ -3837,13 +3923,13 @@ async function beginGithubSignIn(deps, method = "browser") {
3837
3923
  try {
3838
3924
  started = authorization.begin({
3839
3925
  key: GITHUB_RECORD_KEY2,
3840
- method,
3926
+ method: effectiveMethod,
3841
3927
  signal: controller.signal,
3842
3928
  // 浏览器往返靠 authorizeUrl + 回调;notify 只更新内存视图,prompt 不支持(拒绝)。
3843
3929
  interaction: {
3844
3930
  notify: (notice) => {
3845
3931
  if (notice.code !== void 0 && notice.url !== void 0) {
3846
- updateCurrent({ phase: "waiting-device", verificationUri: notice.url, userCode: notice.code });
3932
+ updateCurrent({ phase: "waiting-device", userCode: notice.code });
3847
3933
  } else if (notice.url !== void 0) {
3848
3934
  updateCurrent({ phase: "waiting-browser", authorizeUrl: notice.url });
3849
3935
  }
@@ -4455,6 +4541,7 @@ function githubAuthDeps(deps) {
4455
4541
  get: deps.get,
4456
4542
  ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
4457
4543
  ...deps.githubClientId === void 0 ? {} : { clientId: deps.githubClientId },
4544
+ ...deps.githubClientSecret === void 0 ? {} : { clientSecret: deps.githubClientSecret },
4458
4545
  ...deps.logger === void 0 ? {} : { logger: deps.logger }
4459
4546
  };
4460
4547
  }
@@ -4794,7 +4881,12 @@ async function statePayload(deps, settings) {
4794
4881
  /** 浏览器登录 attempt 视图(idle / waiting-browser / waiting-device / exchanging / done / error)。 */
4795
4882
  attempt: attemptView(),
4796
4883
  /** 浏览器登录是否可用:authorization 服务在、且 GitHub flow 真的注册上了(client_id 有内置默认)。 */
4797
- browserAvailable: browserSignInAvailable(deps)
4884
+ browserAvailable: browserSignInAvailable(deps),
4885
+ /**
4886
+ * 回环 web flow 是否可用:GitHub 换 token 必须有 client_secret,内置 client_id 没有 → 只能设备码。
4887
+ * 前端据此决定主按钮启动哪个 method(false 时直接用 device,避免点了必然失败的 web flow)。
4888
+ */
4889
+ loopbackAvailable: isLoopbackSignInAvailable(githubAuthDeps(deps))
4798
4890
  }
4799
4891
  };
4800
4892
  }
package/lib/index.d.ts CHANGED
@@ -27,6 +27,7 @@ declare function apply(ctx: PluginContext, config?: {
27
27
  stateDir?: string;
28
28
  corePath?: string;
29
29
  githubClientId?: string;
30
+ githubClientSecret?: string;
30
31
  }): void;
31
32
  declare const _default: {
32
33
  name: string;
package/lib/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  createUpdateService,
10
10
  ensureGithubFlow,
11
11
  readPluginInfo
12
- } from "./chunk-5D2VRWPJ.js";
12
+ } from "./chunk-QOYTABJH.js";
13
13
 
14
14
  // src/index.ts
15
15
  var name = "dsh-github-trending";
@@ -20,6 +20,7 @@ function readVersion() {
20
20
  var VERSION = readVersion();
21
21
  function apply(ctx, config = {}) {
22
22
  const env = process.env;
23
+ const githubClientSecret = (config.githubClientSecret ?? env.DSH_GITHUB_CLIENT_SECRET)?.trim();
23
24
  const get = (key) => typeof ctx.get === "function" ? ctx.get(key) : void 0;
24
25
  const log = (event, data) => {
25
26
  const suffix = data === void 0 ? "" : ` ${JSON.stringify(data)}`;
@@ -67,7 +68,8 @@ function apply(ctx, config = {}) {
67
68
  const githubAuth = (resolve) => ({
68
69
  get: resolve,
69
70
  logger: log,
70
- ...config.githubClientId === void 0 ? {} : { clientId: config.githubClientId }
71
+ ...config.githubClientId === void 0 ? {} : { clientId: config.githubClientId },
72
+ ...githubClientSecret === void 0 || githubClientSecret === "" ? {} : { clientSecret: githubClientSecret }
71
73
  });
72
74
  const registerAuth = (resolve) => {
73
75
  if (!ensureGithubFlow(githubAuth(resolve ?? get))) log("auth.flow.deferred", { reason: "authorization-not-ready" });
@@ -100,6 +102,7 @@ function apply(ctx, config = {}) {
100
102
  pluginName: pluginInfo.name,
101
103
  ...pluginInfo.repository === void 0 ? {} : { repository: pluginInfo.repository },
102
104
  ...config.githubClientId === void 0 ? {} : { githubClientId: config.githubClientId },
105
+ ...githubClientSecret === void 0 || githubClientSecret === "" ? {} : { githubClientSecret },
103
106
  logger: log
104
107
  });
105
108
  const register = () => {
package/lib/internal.d.ts CHANGED
@@ -1164,7 +1164,13 @@ declare const CALLBACK_PATH = "/api/morehao/github-trending/oauth/github/callbac
1164
1164
  /**
1165
1165
  * 内置的 GitHub OAuth App 公开 client_id(**非机密**:OAuth 的 client_id 设计上就是公开的,
1166
1166
  * 会出现在授权 URL 里,与 `gh`/VS Code 内置自己的 client_id 同理)。
1167
- * 纯 PKCE public client,无需 client_secret。用户/部署可在 `apply(config.githubClientId)` 覆盖。
1167
+ *
1168
+ * ⚠️ 只有 client_id 不够:GitHub 的 web application flow 在换 token 时**仍然要求
1169
+ * client_secret**(PKCE 是叠加保护,不能替代,2025-07 起官方也只说「推荐」而非「可免 secret」)。
1170
+ * 因此:
1171
+ * - 内置 client_id → 只能走 **device flow**(`resolveMethod` 自动降级),开箱即用;
1172
+ * - 用户自带 OAuth App(`githubClientId` + `githubClientSecret`)→ 才启用回环 web flow。
1173
+ * 且 device flow 需要在 OAuth App 设置里勾选启用(本插件内置的 App 已启用)。
1168
1174
  *
1169
1175
  * OAuth App 侧登记的回调:`http://127.0.0.1/api/morehao/github-trending/oauth/github/callback`
1170
1176
  * (loopback 按 RFC 8252 忽略端口,运行时随机端口可正常回跳)。
@@ -1172,6 +1178,18 @@ declare const CALLBACK_PATH = "/api/morehao/github-trending/oauth/github/callbac
1172
1178
  declare const DEFAULT_GITHUB_CLIENT_ID = "Ov23li7rhp0EKoqbkJKY";
1173
1179
  /** 解析生效的 client_id:显式 config 优先,否则用内置默认。 */
1174
1180
  declare function resolveClientId(deps: GithubAuthDeps): string | undefined;
1181
+ /** 解析生效的 client_secret(没有就返回 undefined,绝不以空串形式发出去)。 */
1182
+ declare function resolveClientSecret(deps: GithubAuthDeps): string | undefined;
1183
+ /**
1184
+ * 回环(浏览器 web flow)是否可用:GitHub 换 token 必须带 client_secret,
1185
+ * 内置的公开 client_id 没有 secret,只能退到设备码。见文件头的说明。
1186
+ */
1187
+ declare function isLoopbackSignInAvailable(deps: GithubAuthDeps): boolean;
1188
+ /**
1189
+ * 实际要跑的方法:`browser` 需要 client_secret,缺省时自动降级为 `device`(开箱即用)。
1190
+ * 显式选择 `device` 的调用不受影响。
1191
+ */
1192
+ declare function resolveMethod(deps: GithubAuthDeps, requested: 'browser' | 'device'): 'browser' | 'device';
1175
1193
  /** 本插件运行需要 push / 建 PR,`read:user` 不够;一次授权到 `repo`。 */
1176
1194
  declare const DEFAULT_SCOPE = "repo";
1177
1195
  interface AuthorizationFlow {
@@ -1229,6 +1247,11 @@ interface GithubAuthDeps {
1229
1247
  };
1230
1248
  /** GitHub OAuth App 的 public client_id(无需 secret)。 */
1231
1249
  clientId?: string;
1250
+ /**
1251
+ * GitHub OAuth App 的 client_secret。只有自带 OAuth App 的回环 web flow 才需要;
1252
+ * 缺省时 `browser` 方法自动降级为 device flow。不进 `settings.json`、不进日志。
1253
+ */
1254
+ clientSecret?: string;
1232
1255
  scope?: string;
1233
1256
  attemptTimeoutMs?: number;
1234
1257
  }
@@ -1237,12 +1260,16 @@ interface GithubAttemptView {
1237
1260
  phase: GithubAttemptPhase;
1238
1261
  authorizeUrl?: string;
1239
1262
  verificationUri?: string;
1263
+ /** device flow 预填验证码的地址(GitHub 返回的 `verification_uri_complete`):打开只需点授权。 */
1264
+ verificationUriComplete?: string;
1240
1265
  userCode?: string;
1241
1266
  expiresAt?: number;
1242
1267
  login?: string;
1243
1268
  error?: string;
1244
1269
  }
1245
1270
  declare function attemptView(): GithubAttemptView;
1271
+ /** 判断一个未知值是否是可直接打开的 http(s) URL(用于校验 GitHub 返回的验证页地址)。 */
1272
+ declare function isHttpUrl(value: unknown): boolean;
1246
1273
  /** PKCE:返回 verifier 与 S256 challenge。 */
1247
1274
  declare function createPkce(): {
1248
1275
  verifier: string;
@@ -1250,6 +1277,13 @@ declare function createPkce(): {
1250
1277
  };
1251
1278
  /** 常量时间比较 state(长度不等直接 false,避免 timingSafeEqual 抛错)。 */
1252
1279
  declare function verifyState(received: string, expected: string): boolean;
1280
+ /** 回调落地页的两种结果(纯展示,无外部资源,离线可测)。 */
1281
+ type CallbackPageKind = 'success' | 'error';
1282
+ /**
1283
+ * 回环回调的落地页 HTML。自包含、无外链资源(CSP 友好、离线可用),
1284
+ * 跟随系统深浅色;成功页会尝试自动关闭本窗口(脚本失败时文字兜底)。
1285
+ */
1286
+ declare function renderCallbackPage(kind: CallbackPageKind): string;
1253
1287
  /** 拼授权页 URL。 */
1254
1288
  declare function buildAuthorizeUrl(params: {
1255
1289
  authorizeEndpoint: string;
@@ -1321,12 +1355,17 @@ declare const githubAuth_cancelGithubSignIn: typeof cancelGithubSignIn;
1321
1355
  declare const githubAuth_createPkce: typeof createPkce;
1322
1356
  declare const githubAuth_ensureGithubFlow: typeof ensureGithubFlow;
1323
1357
  declare const githubAuth_isGithubFlowRegistered: typeof isGithubFlowRegistered;
1358
+ declare const githubAuth_isHttpUrl: typeof isHttpUrl;
1359
+ declare const githubAuth_isLoopbackSignInAvailable: typeof isLoopbackSignInAvailable;
1324
1360
  declare const githubAuth_registerGithubFlow: typeof registerGithubFlow;
1361
+ declare const githubAuth_renderCallbackPage: typeof renderCallbackPage;
1325
1362
  declare const githubAuth_resetAttemptForTest: typeof resetAttemptForTest;
1326
1363
  declare const githubAuth_resolveClientId: typeof resolveClientId;
1364
+ declare const githubAuth_resolveClientSecret: typeof resolveClientSecret;
1365
+ declare const githubAuth_resolveMethod: typeof resolveMethod;
1327
1366
  declare const githubAuth_verifyState: typeof verifyState;
1328
1367
  declare namespace githubAuth {
1329
- export { type githubAuth_AuthorizationFlow as AuthorizationFlow, type githubAuth_AuthorizationLike as AuthorizationLike, type githubAuth_AuthorizationSessionLike as AuthorizationSessionLike, githubAuth_CALLBACK_PATH as CALLBACK_PATH, githubAuth_DEFAULT_GITHUB_CLIENT_ID as DEFAULT_GITHUB_CLIENT_ID, githubAuth_DEFAULT_SCOPE as DEFAULT_SCOPE, githubAuth_GITHUB_RECORD_KEY as GITHUB_RECORD_KEY, type githubAuth_GithubAttemptPhase as GithubAttemptPhase, type githubAuth_GithubAttemptView as GithubAttemptView, type githubAuth_GithubAuthDeps as GithubAuthDeps, type githubAuth_GithubGrantPayload as GithubGrantPayload, type githubAuth_WebServerLike as WebServerLike, githubAuth_attemptView as attemptView, githubAuth_beginGithubSignIn as beginGithubSignIn, githubAuth_buildAuthorizeUrl as buildAuthorizeUrl, githubAuth_cancelGithubSignIn as cancelGithubSignIn, githubAuth_createPkce as createPkce, githubAuth_ensureGithubFlow as ensureGithubFlow, githubAuth_isGithubFlowRegistered as isGithubFlowRegistered, githubAuth_registerGithubFlow as registerGithubFlow, githubAuth_resetAttemptForTest as resetAttemptForTest, githubAuth_resolveClientId as resolveClientId, githubAuth_verifyState as verifyState };
1368
+ export { type githubAuth_AuthorizationFlow as AuthorizationFlow, type githubAuth_AuthorizationLike as AuthorizationLike, type githubAuth_AuthorizationSessionLike as AuthorizationSessionLike, githubAuth_CALLBACK_PATH as CALLBACK_PATH, githubAuth_DEFAULT_GITHUB_CLIENT_ID as DEFAULT_GITHUB_CLIENT_ID, githubAuth_DEFAULT_SCOPE as DEFAULT_SCOPE, githubAuth_GITHUB_RECORD_KEY as GITHUB_RECORD_KEY, type githubAuth_GithubAttemptPhase as GithubAttemptPhase, type githubAuth_GithubAttemptView as GithubAttemptView, type githubAuth_GithubAuthDeps as GithubAuthDeps, type githubAuth_GithubGrantPayload as GithubGrantPayload, type githubAuth_WebServerLike as WebServerLike, githubAuth_attemptView as attemptView, githubAuth_beginGithubSignIn as beginGithubSignIn, githubAuth_buildAuthorizeUrl as buildAuthorizeUrl, githubAuth_cancelGithubSignIn as cancelGithubSignIn, githubAuth_createPkce as createPkce, githubAuth_ensureGithubFlow as ensureGithubFlow, githubAuth_isGithubFlowRegistered as isGithubFlowRegistered, githubAuth_isHttpUrl as isHttpUrl, githubAuth_isLoopbackSignInAvailable as isLoopbackSignInAvailable, githubAuth_registerGithubFlow as registerGithubFlow, githubAuth_renderCallbackPage as renderCallbackPage, githubAuth_resetAttemptForTest as resetAttemptForTest, githubAuth_resolveClientId as resolveClientId, githubAuth_resolveClientSecret as resolveClientSecret, githubAuth_resolveMethod as resolveMethod, githubAuth_verifyState as verifyState };
1330
1369
  }
1331
1370
 
1332
1371
  interface DefaultBranchResult {
@@ -2010,8 +2049,10 @@ interface RouteDeps {
2010
2049
  /** 包名与仓库地址都来自 package.json,由 src/index.ts 传入。 */
2011
2050
  pluginName?: string;
2012
2051
  repository?: string;
2013
- /** 浏览器 GitHub 登录用的 public client_id(由 apply 的 config 传入;缺省时按钮会提示未配置)。 */
2052
+ /** 浏览器 GitHub 登录用的 public client_id(由 apply 的 config 传入;缺省时用内置默认)。 */
2014
2053
  githubClientId?: string;
2054
+ /** 自带 OAuth App 时的 client_secret(只用于回环 web flow;缺省时该 method 自动降级为设备码)。 */
2055
+ githubClientSecret?: string;
2015
2056
  logger?: (event: string, data?: Record<string, unknown>) => void;
2016
2057
  }
2017
2058
  declare function createRoutes(deps: RouteDeps): (request: http.IncomingMessage, response: http.ServerResponse) => Promise<void>;
package/lib/internal.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  tools_exports,
28
28
  update_exports,
29
29
  version_exports
30
- } from "./chunk-5D2VRWPJ.js";
30
+ } from "./chunk-QOYTABJH.js";
31
31
 
32
32
  // src/internal.ts
33
33
  import * as exec from "@morehao/dsh-plugin-kit/exec";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-github-trending",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "GitHub 趋势日报的 DeepSeek Harness 插件:按日 / 周 / 月周期定时 spawn 本地 trendradar 核心,抓取 Trending、生成中文简介与周期总结,并可选把订阅仓库的新 Release 合成中文摘要,一起提交到你的仓库、自动开 PR 合并;内置概览 / 榜单 / 运行记录 / 订阅 / 设置工作台。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",