@wangjunjian/dsh-github-trending 0.1.0 → 0.2.0

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
@@ -27,6 +27,20 @@ In the `web` profile, the plugin additionally renders a dockable right-hand
27
27
  panel (daily/weekly/monthly tabs, resizable, collapsible to a rail) backed by a
28
28
  host-side in-memory cache served over the `/github-trending` HTTP route.
29
29
 
30
+ ### Repository overviews (opt-in, LLM-backed)
31
+
32
+ When `overviewsEnabled` is on, every successful cache refresh asynchronously
33
+ fetches the README of each top repository and asks the harness LLM to write a
34
+ short, localized overview. Generation is sequential (one LLM call at a time),
35
+ starting from the first repository of the window the panel is currently
36
+ viewing; cards show a "generating" indicator while waiting, and the overview
37
+ replaces the repository tagline once ready. Clicking a card's overview area
38
+ opens a wide dialog with a full, uniformly structured Markdown project
39
+ introduction (core positioning / use cases / license), generated on demand and
40
+ cached in memory. The `github_trending` tool output is unchanged.
41
+ This consumes LLM tokens — hence disabled by default. Generation failures are
42
+ logged on the host and retried on the next refresh cycle.
43
+
30
44
  ## Installation
31
45
 
32
46
  Install the plugin into a DeepSeek Harness profile. The plugin does not modify
@@ -71,6 +85,7 @@ profile's `cordis.patch.yml`:
71
85
  timeoutMs: 30000
72
86
  maxResults: 10
73
87
  refreshIntervalMs: 14400000
88
+ overviewsEnabled: false
74
89
  ```
75
90
 
76
91
  | Field | Default | Description |
@@ -79,6 +94,14 @@ profile's `cordis.patch.yml`:
79
94
  | `timeoutMs` | `30000` | Cooperative timeout budget (ms) for each HTTP request. |
80
95
  | `maxResults` | `10` | Maximum repositories the tool may return (clamped to 25). |
81
96
  | `refreshIntervalMs` | `14400000` | Background refresh interval (ms) for the UI cache (4 hours). |
97
+ | `overviewsEnabled` | `false` | Generate LLM README overviews after each refresh. |
98
+ | `overviewsLanguage` | `zh` | Overview language: `zh` or `en`. |
99
+ | `overviewsMaxRepos` | `10` | Only the top N repositories per window get an overview. |
100
+ | `overviewsProvider` / `overviewsModel` | — | Optional LLM route override (must be set together); defaults to the harness default model selection. |
101
+ | `overviewsMaxTokens` | `2048` | Output token cap per overview call (reasoning models need the headroom). |
102
+ | `overviewsTimeoutMs` | `60000` | Per-call timeout (ms) for one README fetch or LLM call. |
103
+ | `introsMaxTokens` | `4096` | Output token cap per on-demand project introduction call. |
104
+ | `introsTimeoutMs` | `120000` | Timeout (ms) for one introduction generation. |
82
105
 
83
106
  ## Development
84
107
 
package/README.zh.md CHANGED
@@ -25,6 +25,18 @@ Trending 页面(按语言和时间窗口),返回结构化的热门仓库
25
25
  (daily/weekly/monthly 标签页、可拖拽调宽、可折叠为窄条),数据来自宿主侧
26
26
  内存缓存,通过 `/github-trending` HTTP 路由提供。
27
27
 
28
+ ### 仓库概览(可选,依赖 LLM)
29
+
30
+ 开启 `overviewsEnabled` 后,每次缓存刷新成功都会异步抓取排名靠前仓库的
31
+ README,并调用 harness 的 LLM 生成一段简短的本地化概览。生成是串行的
32
+ (同时只进行一次 LLM 调用),从面板当前查看窗口的第一个仓库开始依次进行;
33
+ 等待中的卡片会显示"生成中"标志,概览生成后替换仓库的原始简介。点击卡片的
34
+ 概览区域会弹出宽幅对话框,展示统一结构化(一句话定位 / 它是什么 /
35
+ 解决什么 / 怎么用 / 该不该关注)的完整 Markdown 项目介绍——按需生成并
36
+ 缓存在内存中。
37
+ `github_trending` 工具的输出不受影响。概览与介绍都会消耗 LLM token——因此
38
+ 默认关闭。生成失败会在 host 端记录日志,并在下一轮刷新时重试。
39
+
28
40
  ## 安装
29
41
 
30
42
  将插件安装到某个 DeepSeek Harness profile,不会修改 `deepseek-harness`
@@ -68,6 +80,7 @@ bundle patch 默认插入一行配置。可在 profile 的 `cordis.patch.yml`
68
80
  timeoutMs: 30000
69
81
  maxResults: 10
70
82
  refreshIntervalMs: 14400000
83
+ overviewsEnabled: false
71
84
  ```
72
85
 
73
86
  | 字段 | 默认值 | 说明 |
@@ -76,6 +89,14 @@ bundle patch 默认插入一行配置。可在 profile 的 `cordis.patch.yml`
76
89
  | `timeoutMs` | `30000` | 每次 HTTP 请求的协作式超时预算(毫秒)。 |
77
90
  | `maxResults` | `10` | 工具返回仓库数上限(最大会被钳制到 25)。 |
78
91
  | `refreshIntervalMs` | `14400000` | UI 缓存的后台刷新间隔(毫秒,4 小时)。 |
92
+ | `overviewsEnabled` | `false` | 每次刷新后生成基于 README 的 LLM 概览。 |
93
+ | `overviewsLanguage` | `zh` | 概览语言:`zh` 或 `en`。 |
94
+ | `overviewsMaxRepos` | `10` | 每个窗口只为前 N 个仓库生成概览。 |
95
+ | `overviewsProvider` / `overviewsModel` | — | 可选的 LLM 路由覆盖(必须成对配置);默认跟随 harness 默认模型选择。 |
96
+ | `overviewsMaxTokens` | `2048` | 单次概览调用的输出 token 上限(推理模型需要预留思考 token)。 |
97
+ | `overviewsTimeoutMs` | `60000` | 单次 README 抓取或 LLM 调用的超时(毫秒)。 |
98
+ | `introsMaxTokens` | `4096` | 单次按需生成项目介绍的输出 token 上限。 |
99
+ | `introsTimeoutMs` | `120000` | 单次项目介绍生成的超时(毫秒)。 |
79
100
 
80
101
  ## 开发
81
102
 
package/lib/cache.js CHANGED
@@ -32,6 +32,21 @@ export class TrendingCache {
32
32
  get(language, since) {
33
33
  return this.entries.get(cacheKey(language, since));
34
34
  }
35
+ /**
36
+ * Find a repository by full name across all cached entries (any language,
37
+ * any window). Used to attach trending metadata to on-demand generations.
38
+ *
39
+ * @param fullName - `owner/name` repository path.
40
+ * @returns the repository, or undefined when not currently cached.
41
+ */
42
+ findRepository(fullName) {
43
+ for (const entry of this.entries.values()) {
44
+ const repo = entry.repositories.find((candidate) => candidate.fullName === fullName);
45
+ if (repo !== undefined)
46
+ return repo;
47
+ }
48
+ return undefined;
49
+ }
35
50
  /**
36
51
  * Fetch fresh data and store it. Reuses the configured timeout.
37
52
  *
@@ -42,7 +57,20 @@ export class TrendingCache {
42
57
  */
43
58
  async refresh(fetcher, language, since) {
44
59
  const repositories = await this.fetchWithRetry(fetcher);
45
- return this.set(language, since, repositories);
60
+ const entry = this.set(language, since, repositories);
61
+ this.notifyRefreshed(entry, language, since);
62
+ return entry;
63
+ }
64
+ /** Invoke the optional onRefreshed hook, swallowing synchronous failures. */
65
+ notifyRefreshed(entry, language, since) {
66
+ if (this.options.onRefreshed === undefined)
67
+ return;
68
+ try {
69
+ this.options.onRefreshed(entry, language, since);
70
+ }
71
+ catch {
72
+ // A hook failure (e.g. overview generation) must never break caching.
73
+ }
46
74
  }
47
75
  /**
48
76
  * Fetch with a per-attempt timeout, retrying transient failures.
@@ -99,6 +127,7 @@ export class TrendingCache {
99
127
  for (const [since, fetcher] of Object.entries(fetchers)) {
100
128
  const repositories = await this.fetchWithRetry(fetcher);
101
129
  results[since] = this.set(language, since, repositories, cachedAt);
130
+ this.notifyRefreshed(results[since], language, since);
102
131
  }
103
132
  return results;
104
133
  }
package/lib/client.js CHANGED
@@ -20,7 +20,7 @@ window.__ModuleLoader__.load({
20
20
  const DEFAULT_REFRESH_INTERVAL_MS = 144e5;
21
21
  //#endregion
22
22
  //#region \0dsh-css:/Users/junjian/GitHub/wang-junjian/dsh-github-trending/src/client/GithubTrendingPanel.module.css.mjs
23
- const css = ".xbtn-q_overlayRoot{background:var(--dsw-bg-float);border-left:1px solid var(--dsw-alias-border-l2);z-index:100;pointer-events:auto;flex-direction:column;display:flex;position:fixed;top:0;bottom:0;right:0}.xbtn-q_railRoot{background:var(--dsw-bg-float);border:1px solid var(--dsw-alias-border-l2);z-index:100;pointer-events:auto;border-right:none;border-radius:10px 0 0 10px;flex-direction:column;justify-content:center;align-items:center;gap:6px;width:44px;padding:10px 4px;display:flex;position:fixed;top:50%;right:0;transform:translateY(-50%)}.xbtn-q_railButton{width:36px;color:var(--dsw-text-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex-direction:column;justify-content:center;align-items:center;padding:8px 0;transition:background-color .15s,color .15s;display:flex}.xbtn-q_railButton:hover{background:var(--dsw-fill-hover);color:var(--dsw-text-link)}.xbtn-q_railLabel{font:var(--dsw-font-caption);writing-mode:vertical-rl;letter-spacing:.06em}.xbtn-q_resizeHandle{cursor:col-resize;z-index:1;width:8px;position:absolute;top:0;bottom:0;left:-4px}.xbtn-q_header{border-bottom:1px solid var(--dsw-border-subtle);flex:none;justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.xbtn-q_titleWrap{align-items:center;gap:8px;min-width:0;display:flex}.xbtn-q_titleIcon{color:var(--dsw-text-brand);flex:none}.xbtn-q_title{font:var(--dsw-font-title-2);color:var(--dsw-text-primary);margin:0;font-weight:700}.xbtn-q_cachedAt{font:var(--dsw-font-caption);color:var(--dsw-text-tertiary);white-space:nowrap;font-size:10px}.xbtn-q_footer{border-top:1px solid var(--dsw-border-subtle);flex:none;justify-content:flex-end;align-items:center;gap:12px;padding:6px 14px;display:flex}.xbtn-q_headerActions{align-items:center;gap:6px;display:flex}.xbtn-q_iconButton{width:28px;height:28px;color:var(--dsw-text-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;transition:background-color .15s,color .15s;display:flex}.xbtn-q_iconButton:hover{background:var(--dsw-fill-hover);color:var(--dsw-text-primary)}.xbtn-q_iconButton:disabled{opacity:.5;cursor:not-allowed}.xbtn-q_spinning svg{animation:1s linear infinite xbtn-q_dshGithubTrendingSpin}@keyframes xbtn-q_dshGithubTrendingSpin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.xbtn-q_sinceTabs{border-bottom:1px solid var(--dsw-border-subtle);flex:none;align-items:center;gap:6px;padding:10px 14px;display:flex}.xbtn-q_sinceTabGroup{flex:auto;align-items:center;gap:6px;display:flex}.xbtn-q_sinceTab,.xbtn-q_sinceTabActive{border:1px solid var(--dsw-border-default);background:var(--dsw-fill-input);height:30px;color:var(--dsw-text-secondary);font:var(--dsw-font-body-2);cursor:pointer;border-radius:8px;flex:1 1 0;padding:0 8px;transition:background-color .15s,color .15s,border-color .15s}.xbtn-q_sinceTab:hover{background:var(--dsw-fill-hover);color:var(--dsw-text-primary)}.xbtn-q_sinceTabActive{background:var(--dsw-fill-brand);border-color:var(--dsw-border-brand);color:var(--dsw-text-brand);font-weight:500}.xbtn-q_list{flex-direction:column;flex:auto;gap:8px;padding:8px 10px;display:flex;overflow:auto}.xbtn-q_card{border:1px solid var(--dsw-border-subtle);background:var(--dsw-bg-base);color:inherit;border-radius:10px;flex-direction:column;gap:6px;padding:10px;text-decoration:none;transition:border-color .15s;display:flex}.xbtn-q_card:hover{border-color:var(--dsw-border-brand)}.xbtn-q_cardHeader{align-items:center;gap:8px;min-width:0;display:flex}.xbtn-q_rank,.xbtn-q_rankGold,.xbtn-q_rankSilver,.xbtn-q_rankBronze{width:22px;height:22px;font:var(--dsw-font-caption);color:var(--dsw-text-tertiary);background:var(--dsw-fill-hover);border-radius:50%;flex:none;justify-content:center;align-items:center;font-weight:600;display:flex}.xbtn-q_rankGold{color:#7c4a00;background:linear-gradient(135deg,gold,#ffb800)}.xbtn-q_rankSilver{color:#4a4a4a;background:linear-gradient(135deg,#e8e8e8,silver)}.xbtn-q_rankBronze{color:#5e3a18;background:linear-gradient(135deg,#f0a068,#cd7f32)}.xbtn-q_repoName{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:var(--dsw-font-body-1);color:var(--dsw-text-primary);flex:auto;font-weight:500;overflow:hidden}.xbtn-q_card:hover .xbtn-q_repoName{color:var(--dsw-text-link)}.xbtn-q_description{-webkit-line-clamp:2;color:var(--dsw-text-tertiary);font:var(--dsw-font-caption);-webkit-box-orient:vertical;font-size:14px;line-height:1.35;display:-webkit-box;overflow:hidden}.xbtn-q_meta{color:var(--dsw-text-tertiary);font:var(--dsw-font-caption);flex-wrap:wrap;align-items:center;gap:8px;font-size:11px;display:flex}.xbtn-q_metaPill{background:var(--dsw-fill-hover);color:var(--dsw-text-tertiary);border-radius:10px;align-items:center;gap:4px;padding:1px 6px;display:inline-flex}.xbtn-q_metaDot{border-radius:50%;width:7px;height:7px}.xbtn-q_metaItem{align-items:center;gap:3px;display:inline-flex}.xbtn-q_metaToday{color:var(--dsw-text-success);font-weight:500}.xbtn-q_momentum{flex-direction:column;gap:4px;display:flex}.xbtn-q_momentumTrack{background:var(--dsw-fill-input);border:1px solid var(--dsw-border-subtle);border-radius:2.5px;height:5px;overflow:hidden}.xbtn-q_momentumFill{height:100%;transition:width .4s var(--ds-ease-in-out,ease);border-radius:2.5px}.xbtn-q_status{text-align:center;color:var(--dsw-text-secondary);font:var(--dsw-font-body-2);padding:24px 14px}";
23
+ const css = ".xbtn-q_overlayRoot{background:var(--dsw-bg-float);border-left:1px solid var(--dsw-alias-border-l2);z-index:100;pointer-events:auto;flex-direction:column;display:flex;position:fixed;top:0;bottom:0;right:0}.xbtn-q_railRoot{background:var(--dsw-bg-float);border:1px solid var(--dsw-alias-border-l2);z-index:100;pointer-events:auto;border-right:none;border-radius:10px 0 0 10px;flex-direction:column;justify-content:center;align-items:center;gap:6px;width:44px;padding:10px 4px;display:flex;position:fixed;top:50%;right:0;transform:translateY(-50%)}.xbtn-q_railButton{width:36px;color:var(--dsw-text-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex-direction:column;justify-content:center;align-items:center;padding:8px 0;transition:background-color .15s,color .15s;display:flex}.xbtn-q_railButton:hover{background:var(--dsw-fill-hover);color:var(--dsw-text-link)}.xbtn-q_railLabel{font:var(--dsw-font-caption);writing-mode:vertical-rl;letter-spacing:.06em}.xbtn-q_resizeHandle{cursor:col-resize;z-index:1;width:8px;position:absolute;top:0;bottom:0;left:-4px}.xbtn-q_header{border-bottom:1px solid var(--dsw-border-subtle);flex:none;justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.xbtn-q_titleWrap{align-items:center;gap:8px;min-width:0;display:flex}.xbtn-q_titleIcon{color:var(--dsw-text-brand);flex:none}.xbtn-q_title{font:var(--dsw-font-title-2);color:var(--dsw-text-primary);margin:0;font-weight:700}.xbtn-q_cachedAt{font:var(--dsw-font-caption);color:var(--dsw-text-tertiary);white-space:nowrap;font-size:10px}.xbtn-q_footer{border-top:1px solid var(--dsw-border-subtle);flex:none;justify-content:flex-end;align-items:center;gap:12px;padding:6px 14px;display:flex}.xbtn-q_headerActions{align-items:center;gap:6px;display:flex}.xbtn-q_iconButton{width:28px;height:28px;color:var(--dsw-text-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;transition:background-color .15s,color .15s;display:flex}.xbtn-q_iconButton:hover{background:var(--dsw-fill-hover);color:var(--dsw-text-primary)}.xbtn-q_iconButton:disabled{opacity:.5;cursor:not-allowed}.xbtn-q_spinning svg{animation:1s linear infinite xbtn-q_dshGithubTrendingSpin}@keyframes xbtn-q_dshGithubTrendingSpin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.xbtn-q_sinceTabs{border-bottom:1px solid var(--dsw-border-subtle);flex:none;align-items:center;gap:6px;padding:10px 14px;display:flex}.xbtn-q_sinceTabGroup{flex:auto;align-items:center;gap:6px;display:flex}.xbtn-q_sinceTab,.xbtn-q_sinceTabActive{border:1px solid var(--dsw-border-default);background:var(--dsw-fill-input);height:30px;color:var(--dsw-text-secondary);font:var(--dsw-font-body-2);cursor:pointer;border-radius:8px;flex:1 1 0;padding:0 8px;transition:background-color .15s,color .15s,border-color .15s}.xbtn-q_sinceTab:hover{background:var(--dsw-fill-hover);color:var(--dsw-text-primary)}.xbtn-q_sinceTabActive{background:var(--dsw-fill-brand);border-color:var(--dsw-border-brand);color:var(--dsw-text-brand);font-weight:500}.xbtn-q_list{flex-direction:column;flex:auto;gap:8px;padding:8px 10px;display:flex;overflow:auto}.xbtn-q_card{border:1px solid var(--dsw-border-subtle);background:var(--dsw-bg-base);color:inherit;border-radius:10px;flex-direction:column;gap:6px;padding:10px;text-decoration:none;transition:border-color .15s;display:flex}.xbtn-q_card:hover{border-color:var(--dsw-border-brand)}.xbtn-q_cardLink{min-width:0;color:inherit;text-decoration:none;display:block}.xbtn-q_cardHeader{align-items:center;gap:8px;min-width:0;display:flex}.xbtn-q_rank,.xbtn-q_rankGold,.xbtn-q_rankSilver,.xbtn-q_rankBronze{width:22px;height:22px;font:var(--dsw-font-caption);color:var(--dsw-text-tertiary);background:var(--dsw-fill-hover);border-radius:50%;flex:none;justify-content:center;align-items:center;font-weight:600;display:flex}.xbtn-q_rankGold{color:#7c4a00;background:linear-gradient(135deg,gold,#ffb800)}.xbtn-q_rankSilver{color:#4a4a4a;background:linear-gradient(135deg,#e8e8e8,silver)}.xbtn-q_rankBronze{color:#5e3a18;background:linear-gradient(135deg,#f0a068,#cd7f32)}.xbtn-q_repoName{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:var(--dsw-font-body-1);color:var(--dsw-text-primary);flex:auto;font-weight:500;overflow:hidden}.xbtn-q_card:hover .xbtn-q_repoName{color:var(--dsw-text-link)}.xbtn-q_repoNameGenerating{animation:1.4s ease-in-out infinite xbtn-q_dshGithubTrendingTitleBreath}@keyframes xbtn-q_dshGithubTrendingTitleBreath{0%,to{color:var(--dsw-alias-label-brand,var(--dsw-text-link,#7a9eff));opacity:1}50%{color:var(--dsw-alias-label-primary,var(--dsw-text-primary,currentColor));opacity:.55}}.xbtn-q_description{-webkit-line-clamp:2;color:var(--dsw-text-tertiary);font:var(--dsw-font-caption);-webkit-box-orient:vertical;font-size:14px;line-height:1.35;display:-webkit-box;overflow:hidden}.xbtn-q_overview{-webkit-line-clamp:4;color:var(--dsw-text-secondary);font:var(--dsw-font-caption);-webkit-box-orient:vertical;font-size:14px;line-height:1.4;display:-webkit-box;overflow:hidden}.xbtn-q_overviewPending{color:var(--dsw-text-tertiary);font:var(--dsw-font-caption);font-size:12px;line-height:1.4;animation:1.6s ease-in-out infinite xbtn-q_dshGithubTrendingPendingPulse}@keyframes xbtn-q_dshGithubTrendingPendingPulse{0%,to{opacity:.45}50%{opacity:1}}.xbtn-q_overviewArea{cursor:pointer;appearance:none;text-align:left;background:0 0;border:none;border-radius:6px;width:calc(100% + 8px);margin:0 -4px;padding:0 4px}.xbtn-q_overviewArea:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}.xbtn-q_introDialog{gap:0;width:min(760px,92vw);max-height:min(84vh,860px);padding-bottom:0}.xbtn-q_introAccent{flex:none;height:3px}.xbtn-q_introHeader{flex:none;align-items:center;gap:12px;padding:16px 20px 10px;display:flex}.xbtn-q_introAvatar{background:var(--dsw-alias-bg-layer-1,#7f7f7f1f);border-radius:8px;flex:none;width:36px;height:36px}.xbtn-q_introTitleWrap{flex-direction:column;flex:auto;gap:4px;min-width:0;display:flex}.xbtn-q_introTitle{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,currentColor);font-size:15px;font-weight:600;text-decoration:none;overflow:hidden}.xbtn-q_introTitle:hover{color:var(--dsw-alias-label-brand,#7a9eff)}.xbtn-q_introMeta{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.xbtn-q_introClose{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary,currentColor);background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;display:inline-flex}.xbtn-q_introClose:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}.xbtn-q_introScroll{flex:auto;min-height:0;padding:6px 20px 22px;font-size:14px;line-height:1.7;overflow-y:auto}.xbtn-q_introStatus{color:var(--dsw-text-tertiary);justify-content:center;align-items:center;gap:8px;padding:32px 0;font-size:13px;display:flex}.xbtn-q_introRetry{border:1px solid var(--dsw-alias-border-l2,#7f7f7f4d);color:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:2px 10px;font-size:12px}.xbtn-q_introRetry:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}.xbtn-q_meta{color:var(--dsw-text-tertiary);font:var(--dsw-font-caption);flex-wrap:wrap;align-items:center;gap:8px;font-size:11px;display:flex}.xbtn-q_metaPill{background:var(--dsw-fill-hover);color:var(--dsw-text-tertiary);border-radius:10px;align-items:center;gap:4px;padding:1px 6px;display:inline-flex}.xbtn-q_metaDot{border-radius:50%;width:7px;height:7px}.xbtn-q_metaItem{align-items:center;gap:3px;display:inline-flex}.xbtn-q_metaToday{color:var(--dsw-text-success);font-weight:500}.xbtn-q_momentum{flex-direction:column;gap:4px;display:flex}.xbtn-q_momentumTrack{background:var(--dsw-fill-input);border:1px solid var(--dsw-border-subtle);border-radius:2.5px;height:5px;overflow:hidden}.xbtn-q_momentumFill{height:100%;transition:width .4s var(--ds-ease-in-out,ease);border-radius:2.5px}.xbtn-q_status{text-align:center;color:var(--dsw-text-secondary);font:var(--dsw-font-body-2);padding:24px 14px}";
24
24
  const tagId = "@wangjunjian/dsh-github-trending/GithubTrendingPanel.module.css";
25
25
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
26
26
  const tag = document.createElement("style");
@@ -33,12 +33,26 @@ window.__ModuleLoader__.load({
33
33
  "cachedAt": "xbtn-q_cachedAt",
34
34
  "card": "xbtn-q_card",
35
35
  "cardHeader": "xbtn-q_cardHeader",
36
+ "cardLink": "xbtn-q_cardLink",
36
37
  "description": "xbtn-q_description",
38
+ "dshGithubTrendingPendingPulse": "xbtn-q_dshGithubTrendingPendingPulse",
37
39
  "dshGithubTrendingSpin": "xbtn-q_dshGithubTrendingSpin",
40
+ "dshGithubTrendingTitleBreath": "xbtn-q_dshGithubTrendingTitleBreath",
38
41
  "footer": "xbtn-q_footer",
39
42
  "header": "xbtn-q_header",
40
43
  "headerActions": "xbtn-q_headerActions",
41
44
  "iconButton": "xbtn-q_iconButton",
45
+ "introAccent": "xbtn-q_introAccent",
46
+ "introAvatar": "xbtn-q_introAvatar",
47
+ "introClose": "xbtn-q_introClose",
48
+ "introDialog": "xbtn-q_introDialog",
49
+ "introHeader": "xbtn-q_introHeader",
50
+ "introMeta": "xbtn-q_introMeta",
51
+ "introRetry": "xbtn-q_introRetry",
52
+ "introScroll": "xbtn-q_introScroll",
53
+ "introStatus": "xbtn-q_introStatus",
54
+ "introTitle": "xbtn-q_introTitle",
55
+ "introTitleWrap": "xbtn-q_introTitleWrap",
42
56
  "list": "xbtn-q_list",
43
57
  "meta": "xbtn-q_meta",
44
58
  "metaDot": "xbtn-q_metaDot",
@@ -49,6 +63,9 @@ window.__ModuleLoader__.load({
49
63
  "momentumFill": "xbtn-q_momentumFill",
50
64
  "momentumTrack": "xbtn-q_momentumTrack",
51
65
  "overlayRoot": "xbtn-q_overlayRoot",
66
+ "overview": "xbtn-q_overview",
67
+ "overviewArea": "xbtn-q_overviewArea",
68
+ "overviewPending": "xbtn-q_overviewPending",
52
69
  "railButton": "xbtn-q_railButton",
53
70
  "railLabel": "xbtn-q_railLabel",
54
71
  "railRoot": "xbtn-q_railRoot",
@@ -57,6 +74,7 @@ window.__ModuleLoader__.load({
57
74
  "rankGold": "xbtn-q_rankGold",
58
75
  "rankSilver": "xbtn-q_rankSilver",
59
76
  "repoName": "xbtn-q_repoName",
77
+ "repoNameGenerating": "xbtn-q_repoNameGenerating",
60
78
  "resizeHandle": "xbtn-q_resizeHandle",
61
79
  "sinceTab": "xbtn-q_sinceTab",
62
80
  "sinceTabActive": "xbtn-q_sinceTabActive",
@@ -156,6 +174,8 @@ window.__ModuleLoader__.load({
156
174
  ];
157
175
  /** Polling interval while the panel is expanded: matches the default host cache refresh interval. */
158
176
  const POLL_INTERVAL_MS = DEFAULT_REFRESH_INTERVAL_MS;
177
+ /** Backoff before retrying a failed overview long-poll request. */
178
+ const PENDING_RETRY_INTERVAL_MS = 5e3;
159
179
  function formatCachedAt(iso) {
160
180
  if (iso === void 0) return "";
161
181
  const date = new Date(iso);
@@ -240,16 +260,60 @@ window.__ModuleLoader__.load({
240
260
  });
241
261
  const [loading, setLoading] = (0, react.useState)(false);
242
262
  const [error, setError] = (0, react.useState)(false);
263
+ const [intro, setIntro] = (0, react.useState)(void 0);
264
+ const introCache = (0, react.useRef)(/* @__PURE__ */ new Map());
243
265
  const resizeStart = (0, react.useRef)(void 0);
244
266
  const rootRef = useLayoutPush(!state.collapsed, state.width);
267
+ /**
268
+ * Open the project-introduction dialog for a repository. Served from the
269
+ * client-side cache when available; otherwise requests an on-demand
270
+ * generation from the host (failures are not cached, so retry works).
271
+ */
272
+ const openIntro = (0, react.useCallback)((repo) => {
273
+ const cached = introCache.current.get(repo.fullName);
274
+ if (cached !== void 0) {
275
+ setIntro({
276
+ repo,
277
+ status: "ready",
278
+ text: cached
279
+ });
280
+ return;
281
+ }
282
+ setIntro({
283
+ repo,
284
+ status: "loading"
285
+ });
286
+ (async () => {
287
+ try {
288
+ const response = await fetch(`/github-trending?intro=${encodeURIComponent(repo.fullName)}`, { headers: { Accept: "application/json" } });
289
+ const data = await response.json();
290
+ if (!response.ok || data.introduction === void 0) throw new Error(data.error ?? `HTTP ${response.status}`);
291
+ introCache.current.set(repo.fullName, data.introduction);
292
+ setIntro((prev) => prev?.repo.fullName === repo.fullName ? {
293
+ repo,
294
+ status: "ready",
295
+ text: data.introduction
296
+ } : prev);
297
+ } catch {
298
+ setIntro((prev) => prev?.repo.fullName === repo.fullName ? {
299
+ repo,
300
+ status: "error"
301
+ } : prev);
302
+ }
303
+ })();
304
+ }, []);
245
305
  (0, react.useEffect)(() => panelStore.subscribe(() => {
246
306
  setState(panelStore.getSnapshot());
247
307
  }), []);
248
308
  const currentEntry = entries[state.since];
249
- const fetchEntry = (0, react.useCallback)(async (since, refresh = false) => {
309
+ const fetchEntry = (0, react.useCallback)(async (since, refresh = false, signal, wait = false) => {
250
310
  const params = new URLSearchParams({ since });
251
311
  if (refresh) params.set("refresh", "1");
252
- const response = await fetch(`/github-trending?${params.toString()}`, { headers: { Accept: "application/json" } });
312
+ if (wait) params.set("wait", "1");
313
+ const response = await fetch(`/github-trending?${params.toString()}`, {
314
+ headers: { Accept: "application/json" },
315
+ signal
316
+ });
253
317
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
254
318
  return await response.json();
255
319
  }, []);
@@ -316,6 +380,35 @@ window.__ModuleLoader__.load({
316
380
  state.collapsed,
317
381
  load
318
382
  ]);
383
+ const hasPendingOverviews = currentEntry?.repositories.some((repo) => repo.overviewPending) ?? false;
384
+ (0, react.useEffect)(() => {
385
+ if (!state.open || state.collapsed || !hasPendingOverviews) return;
386
+ const controller = new AbortController();
387
+ const since = state.since;
388
+ (async () => {
389
+ for (;;) try {
390
+ const data = await fetchEntry(since, false, controller.signal, true);
391
+ if (controller.signal.aborted) return;
392
+ setEntries((prev) => ({
393
+ ...prev,
394
+ [since]: data
395
+ }));
396
+ if (!data.repositories.some((repo) => repo.overviewPending)) return;
397
+ } catch {
398
+ if (controller.signal.aborted) return;
399
+ await new Promise((resolve) => setTimeout(resolve, PENDING_RETRY_INTERVAL_MS));
400
+ }
401
+ })();
402
+ return () => {
403
+ controller.abort();
404
+ };
405
+ }, [
406
+ state.open,
407
+ state.collapsed,
408
+ hasPendingOverviews,
409
+ state.since,
410
+ fetchEntry
411
+ ]);
319
412
  const onResizeStart = (0, react.useCallback)((event) => {
320
413
  resizeStart.current = {
321
414
  x: event.clientX,
@@ -438,28 +531,40 @@ window.__ModuleLoader__.load({
438
531
  currentEntry?.repositories.map((repo) => {
439
532
  const momentum = maxMomentum > 0 ? repo.starsToday / maxMomentum * 100 : 0;
440
533
  const langColor = languageColor(repo.language);
534
+ const summary = repo.overview !== void 0 && repo.overview !== "" ? repo.overview : repo.description;
441
535
  const rankClass = repo.rank === 1 ? GithubTrendingPanel_module_css_default.rankGold : repo.rank === 2 ? GithubTrendingPanel_module_css_default.rankSilver : repo.rank === 3 ? GithubTrendingPanel_module_css_default.rankBronze : GithubTrendingPanel_module_css_default.rank;
442
- return (0, react_jsx_runtime.jsxs)("a", {
443
- href: repo.url,
444
- target: "_blank",
445
- rel: "noreferrer",
536
+ return (0, react_jsx_runtime.jsxs)("div", {
446
537
  className: GithubTrendingPanel_module_css_default.card,
447
- title: repo.fullName,
448
538
  children: [
449
- (0, react_jsx_runtime.jsxs)("div", {
450
- className: GithubTrendingPanel_module_css_default.cardHeader,
451
- children: [(0, react_jsx_runtime.jsx)("span", {
452
- className: rankClass,
453
- children: repo.rank
454
- }), (0, react_jsx_runtime.jsx)("span", {
455
- className: GithubTrendingPanel_module_css_default.repoName,
456
- children: repo.fullName
457
- })]
458
- }),
459
- repo.description !== void 0 && repo.description !== "" && (0, react_jsx_runtime.jsx)("div", {
460
- className: GithubTrendingPanel_module_css_default.description,
461
- children: repo.description
539
+ (0, react_jsx_runtime.jsx)("a", {
540
+ href: repo.url,
541
+ target: "_blank",
542
+ rel: "noreferrer",
543
+ className: GithubTrendingPanel_module_css_default.cardLink,
544
+ title: repo.fullName,
545
+ children: (0, react_jsx_runtime.jsxs)("div", {
546
+ className: GithubTrendingPanel_module_css_default.cardHeader,
547
+ children: [(0, react_jsx_runtime.jsx)("span", {
548
+ className: rankClass,
549
+ children: repo.rank
550
+ }), (0, react_jsx_runtime.jsx)("span", {
551
+ className: repo.overviewGenerating === true ? `${GithubTrendingPanel_module_css_default.repoName} ${GithubTrendingPanel_module_css_default.repoNameGenerating}` : GithubTrendingPanel_module_css_default.repoName,
552
+ children: repo.fullName
553
+ })]
554
+ })
462
555
  }),
556
+ summary !== void 0 && summary !== "" ? (0, react_jsx_runtime.jsx)("button", {
557
+ type: "button",
558
+ className: `${GithubTrendingPanel_module_css_default.overviewArea} ${repo.overview !== void 0 && repo.overview !== "" ? GithubTrendingPanel_module_css_default.overview : GithubTrendingPanel_module_css_default.description}`,
559
+ title: t("intro.hint"),
560
+ onClick: () => {
561
+ openIntro(repo);
562
+ },
563
+ children: summary
564
+ }) : repo.overviewPending === true ? (0, react_jsx_runtime.jsx)("div", {
565
+ className: GithubTrendingPanel_module_css_default.overviewPending,
566
+ children: t("panel.overviewPending")
567
+ }) : null,
463
568
  (0, react_jsx_runtime.jsxs)("div", {
464
569
  className: GithubTrendingPanel_module_css_default.meta,
465
570
  children: [
@@ -508,6 +613,98 @@ window.__ModuleLoader__.load({
508
613
  className: GithubTrendingPanel_module_css_default.cachedAt,
509
614
  children: currentEntry?.cachedAt !== void 0 ? `${t("panel.cachedAt")} ${formatCachedAt(currentEntry.cachedAt)}` : ""
510
615
  })
616
+ }),
617
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
618
+ open: intro !== void 0,
619
+ onClose: () => {
620
+ setIntro(void 0);
621
+ },
622
+ title: intro?.repo.fullName ?? "",
623
+ closeLabel: t("intro.close"),
624
+ headless: true,
625
+ className: GithubTrendingPanel_module_css_default.introDialog,
626
+ children: intro !== void 0 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
627
+ (0, react_jsx_runtime.jsx)("div", {
628
+ className: GithubTrendingPanel_module_css_default.introAccent,
629
+ style: { background: languageColor(intro.repo.language) }
630
+ }),
631
+ (0, react_jsx_runtime.jsxs)("div", {
632
+ className: GithubTrendingPanel_module_css_default.introHeader,
633
+ children: [
634
+ (0, react_jsx_runtime.jsx)("img", {
635
+ className: GithubTrendingPanel_module_css_default.introAvatar,
636
+ src: `https://avatars.githubusercontent.com/${intro.repo.owner}?s=72`,
637
+ alt: ""
638
+ }),
639
+ (0, react_jsx_runtime.jsxs)("div", {
640
+ className: GithubTrendingPanel_module_css_default.introTitleWrap,
641
+ children: [(0, react_jsx_runtime.jsx)("a", {
642
+ className: GithubTrendingPanel_module_css_default.introTitle,
643
+ href: intro.repo.url,
644
+ target: "_blank",
645
+ rel: "noreferrer",
646
+ children: intro.repo.fullName
647
+ }), (0, react_jsx_runtime.jsxs)("div", {
648
+ className: GithubTrendingPanel_module_css_default.introMeta,
649
+ children: [
650
+ intro.repo.language !== void 0 && intro.repo.language !== "" && (0, react_jsx_runtime.jsxs)("span", {
651
+ className: GithubTrendingPanel_module_css_default.metaPill,
652
+ children: [(0, react_jsx_runtime.jsx)("span", {
653
+ className: GithubTrendingPanel_module_css_default.metaDot,
654
+ style: { background: languageColor(intro.repo.language) }
655
+ }), intro.repo.language]
656
+ }),
657
+ intro.repo.stars > 0 && (0, react_jsx_runtime.jsxs)("span", {
658
+ className: GithubTrendingPanel_module_css_default.metaItem,
659
+ children: ["★ ", formatNumber(intro.repo.stars)]
660
+ }),
661
+ intro.repo.forks > 0 && (0, react_jsx_runtime.jsxs)("span", {
662
+ className: GithubTrendingPanel_module_css_default.metaItem,
663
+ children: ["⑂ ", formatNumber(intro.repo.forks)]
664
+ }),
665
+ intro.repo.starsToday > 0 && (0, react_jsx_runtime.jsxs)("span", {
666
+ className: `${GithubTrendingPanel_module_css_default.metaItem} ${GithubTrendingPanel_module_css_default.metaToday}`,
667
+ children: ["+", formatNumber(intro.repo.starsToday)]
668
+ })
669
+ ]
670
+ })]
671
+ }),
672
+ (0, react_jsx_runtime.jsx)("button", {
673
+ type: "button",
674
+ className: GithubTrendingPanel_module_css_default.introClose,
675
+ "aria-label": t("intro.close"),
676
+ onClick: () => {
677
+ setIntro(void 0);
678
+ },
679
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 14 })
680
+ })
681
+ ]
682
+ }),
683
+ (0, react_jsx_runtime.jsxs)("div", {
684
+ className: GithubTrendingPanel_module_css_default.introScroll,
685
+ children: [
686
+ intro.status === "loading" && (0, react_jsx_runtime.jsxs)("div", {
687
+ className: GithubTrendingPanel_module_css_default.introStatus,
688
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutline16, {
689
+ size: 16,
690
+ className: GithubTrendingPanel_module_css_default.spinning
691
+ }), t("intro.loading")]
692
+ }),
693
+ intro.status === "error" && (0, react_jsx_runtime.jsxs)("div", {
694
+ className: GithubTrendingPanel_module_css_default.introStatus,
695
+ children: [t("intro.error"), (0, react_jsx_runtime.jsx)("button", {
696
+ type: "button",
697
+ className: GithubTrendingPanel_module_css_default.introRetry,
698
+ onClick: () => {
699
+ openIntro(intro.repo);
700
+ },
701
+ children: t("intro.retry")
702
+ })]
703
+ }),
704
+ intro.status === "ready" && intro.text !== void 0 && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, { text: intro.text })
705
+ ]
706
+ })
707
+ ] })
511
708
  })
512
709
  ]
513
710
  });
@@ -533,6 +730,12 @@ window.__ModuleLoader__.load({
533
730
  "panel.cachedAt": "Updated",
534
731
  "panel.autoRefresh": "Auto",
535
732
  "panel.collapse": "Collapse",
733
+ "panel.overviewPending": "Generating AI overview…",
734
+ "intro.hint": "Click for a full project introduction",
735
+ "intro.loading": "Generating project introduction…",
736
+ "intro.error": "Failed to generate the introduction.",
737
+ "intro.retry": "Retry",
738
+ "intro.close": "Close",
536
739
  "repo.stars": "stars",
537
740
  "repo.forks": "forks",
538
741
  "repo.today": "today"
@@ -551,6 +754,12 @@ window.__ModuleLoader__.load({
551
754
  "panel.cachedAt": "更新于",
552
755
  "panel.autoRefresh": "自动",
553
756
  "panel.collapse": "收起",
757
+ "panel.overviewPending": "AI 概览生成中…",
758
+ "intro.hint": "点击查看完整项目介绍",
759
+ "intro.loading": "正在生成项目介绍…",
760
+ "intro.error": "项目介绍生成失败。",
761
+ "intro.retry": "重试",
762
+ "intro.close": "关闭",
554
763
  "repo.stars": "星标",
555
764
  "repo.forks": "分支",
556
765
  "repo.today": "今日"