@morehao/dsh-github-trending 0.7.8 → 0.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.7.10] - 2026-11-01
4
+
5
+ > 修复 peer / engine 版本区间漏掉 `0.2.2-*` 预发布的问题,让插件在宿主升到 `0.2.2` 线后仍能正常安装。
6
+
7
+ ### 修复 — 预发布版本被静默排除
8
+
9
+ - `package.json` 的 `peerDependencies["@deepseek-ai/dsh-tools"]` 与 `engines.dsh` 由 `>=0.2.1-rc.3 <0.3.0-0` 改为 `>=0.2.1-rc.3 <0.3.0-0 || >=0.2.2-alpha.0 <0.3.0-0`。
10
+ - 原因:node-semver 只有在范围的某个比较符与版本的 `major.minor.patch` 元组一致、且该比较符自身带预发布标签时才放行预发布版。原范围只在 `0.2.1` 元组上带预发布标签,于是 `0.2.2-alpha.2`、`0.2.9-rc.1` 等非 `0.2.1` 元组的预发布版会被静默排除,用户 `npm install` 时撞上 `ERESOLVE`。
11
+ - 补一条 `0.2.2` 元组的显式预发布分支后,已发布的 `0.2.2-alpha.2` 与后续 `0.2.2-*` 均可正常匹配;`0.3.0-*` 仍被排除。
12
+
13
+ ## [v0.7.9] - 2026-11-01
14
+
15
+ > 内部重构:把重复的通用实现收敛到共享底座 `@morehao/dsh-plugin-kit@^0.3.1`。对外接口与业务语义保持不变。
16
+
17
+ ### 改进 — 迁移到 dsh-plugin-kit 0.3.1
18
+
19
+ - **子进程执行**:`src/core/exec.ts` 收敛到 kit `./exec` 的 `spawnCollect` / `sanitizedEnv`,删除本地实现;握手阶段改为判 `result.error` 抛 `CORE_SPAWN_FAILED`(kit 的 `spawnCollect` 永不 reject)。
20
+ - **插件身份**:`plugin-info.ts` 的仓库地址规范化与读包改用 kit `./github` + `./meta`(`createPluginMeta`),保留 `readPluginInfo` / `normalizeRepositoryUrl` 导出面与返回形状。
21
+ - **HTTP 路由**:`routes.ts` 的 `sendJson` / `readBody` / 头校验改用 kit `./http` 的 `createRouteHandler`;业务 `dispatch`、`IpcError` 错误码与 `PluginUpdateError→409` 映射保持不变,错误码翻译仍留在插件侧。
22
+ - 对外路由响应形状、读包字段、错误码/映射均无变化(行为等价)。
23
+
3
24
  ## [v0.7.8] - 2026-11-01
4
25
 
5
26
  > 统一「订阅」口径:这个功能不再叫「发版」,运行记录、概览、设置、订阅页与提示文案一致;顺带修好运行记录里的类型胶囊样式。
package/README.md CHANGED
@@ -1,21 +1,61 @@
1
- # dsh-github-trending
1
+ <p align="center">
2
+ <img src="icon.svg" alt="dsh-github-trending logo" width="88" height="88">
3
+ </p>
2
4
 
3
- [![check](https://github.com/morehao/dsh-github-trending/actions/workflows/check.yml/badge.svg)](https://github.com/morehao/dsh-github-trending/actions/workflows/check.yml)
4
- [![npm version](https://img.shields.io/npm/v/@morehao/dsh-github-trending.svg)](https://www.npmjs.com/package/@morehao/dsh-github-trending)
5
+ <h1 align="center">dsh-github-trending</h1>
5
6
 
6
- > GitHub 趋势榜的 **DSH 入口**:按日/周/月周期定时 spawn 本地 [`trendradar`](https://github.com/morehao/trendradar) 核心,抓取 Trending、生成中文简介与周期总结,提交到你自己的仓库并自动合并 PR。
7
+ <p align="center">
8
+ GitHub 趋势榜的 DeepSeek Harness 插件
9
+ </p>
7
10
 
8
- **状态:0.3.0。** 插件侧(配置、调度、核心调用、状态与工作台、两个工具)已完成。核心二进制走 **GitHub Release**:首次运行按 `os/arch` 下载、用 `checksums.txt` 校验 sha256、缓存到插件数据目录,之后离线复用。核心仓库打 tag 出资产之前,运行会明确报 `CORE_NOT_DOWNLOADED` / `CORE_UNAVAILABLE` 并给出下载地址——不会写空文件,也不会退回旧内容。
11
+ <p align="center">
12
+ <a href="https://github.com/morehao/dsh-github-trending/actions/workflows/check.yml"><img alt="check" src="https://github.com/morehao/dsh-github-trending/actions/workflows/check.yml/badge.svg"></a>
13
+ <a href="https://www.npmjs.com/package/@morehao/dsh-github-trending"><img alt="npm version" src="https://img.shields.io/npm/v/@morehao/dsh-github-trending.svg"></a>
14
+ </p>
9
15
 
10
- ## 它做什么
16
+ GitHub 趋势榜的 **DeepSeek Harness 插件**:按日 / 周 / 月周期定时生成趋势日报,交给本地 [`trendradar`](https://github.com/morehao/trendradar) 核心抓取 Trending、写中文简介与周期总结,并自动提交到你自己的仓库、开 PR 合并。你在工作台里看状态、读榜单、管理订阅,不用盯命令行。
11
17
 
12
- - 按你设定的时间(IANA 时区)每天跑一次;从侧栏入口进入工作台,点「立即运行」
13
- - 一次运行 = **spawn 一次本地核心**:抓取 → 模型总结 → 渲染 → commit → push → PR → 合并,全部由核心完成
14
- - 插件只负责:收集配置、调度、传配置与密钥、展示结果与错误、记录最近 90 次运行
15
- - 内容按核心产出的成品**原样**落地(canonical),插件不改写、不重排、不二次调用模型
16
- - 工作台分「概览 / 榜单 / 运行记录 / 设置」:概览回答下次运行、就绪检查与上次交付;榜单按周期(日 / 周 / 月)分别回看;运行记录可展开交付物、按周期筛选并看只读日志
18
+ ## 定位
17
19
 
18
- **它不做什么**:不抓 GitHub、不解析 HTML、不调模型(回环代理模式除外)、不渲染内容、不执行 git。这些只有核心一处实现,避免两份实现漂移。
20
+ - **它是入口,不是引擎**。抓取、解析、模型总结、渲染、git 与 PR 全在本地 `trendradar` 核心一处实现;插件只负责**配置、调度、spawn 核心、展示结果**,避免两份实现漂移。
21
+ - **成品原样落地**。内容按核心产出的 canonical 逐字节写入,插件不改写、不重排、不二次调用模型。
22
+ - **本地运行、无遥测**。核心在你的机器上跑;插件没有云端账号,密钥只经运行时通道交给核心,不写配置文件、不进日志。
23
+
24
+ > 需要什么、配置项、错误码、订阅阶段、实现细节等深入内容见 **[docs/reference.md](docs/reference.md)**。
25
+
26
+ ## 功能说明
27
+
28
+ 工作台从侧栏「GitHub 趋势」或 `设置 → GitHub 趋势` 进入,分五个视图:
29
+
30
+ ### 概览
31
+
32
+ 一屏回答「下次什么时候跑、四项就绪检查是否齐、上次交付了什么」。顶部是插件身份条(版本 / 包名 / 检查更新),右侧常驻下次运行与就绪 N/4。
33
+
34
+ ![概览](docs/images/overview.png)
35
+
36
+ ### 榜单
37
+
38
+ 按周期(日榜 / 周榜 / 月榜)回看历史榜单,直接渲染 Markdown(标题、列表、表格、链接),也可切「原文」。只有保留了预览副本的期数能读;失败 / 无变更 / 试跑 / 旧记录各有空状态。
39
+
40
+ ![榜单](docs/images/digest.png)
41
+
42
+ ### 运行记录
43
+
44
+ 最近 30 次运行,按成功 / 失败 / 试跑筛选;展开某条看交付物(提交、内容哈希、分支、PR 链接)与只读日志,页脚给累计 token。
45
+
46
+ ![运行记录](docs/images/runs.png)
47
+
48
+ ### 设置
49
+
50
+ 内容来源 / 模型 / 仓库 / 认证 / 榜单 / 调度 六个分区,每个分区独立保存与校验;某分区有未保存改动时左侧对应项显示黄点。
51
+
52
+ ![设置](docs/images/settings.png)
53
+
54
+ ### 订阅
55
+
56
+ (可选)订阅一组仓库,每周把每个仓库当周新增的 Release 合成一段中文摘要,与榜单同分支、同 PR 一起提交;订阅列表随数据仓版本化,可审计、可复现。
57
+
58
+ ![订阅](docs/images/subscriptions.png)
19
59
 
20
60
  ## 安装
21
61
 
@@ -23,173 +63,25 @@
23
63
  dsh plugin --profile <profile> add @morehao/dsh-github-trending
24
64
  ```
25
65
 
26
- 装完重启宿主,在「设置 → GitHub 趋势」或侧栏入口进入。
66
+ 装完重启宿主,从侧栏「GitHub 趋势」或 `设置 → GitHub 趋势` 进入。
27
67
 
28
- ## 三步上手
68
+ ### 三步上手
29
69
 
30
70
  1. **选仓库**:填本地 git 仓库的绝对路径(插件不替你 `git init`,不是仓库就报 `REPO_NOT_GIT`)。
31
71
  2. **填模型**:点「沿用 DSH 配置」从宿主 provider 目录里挑 provider 与模型;可导出密钥的直接传给核心,OAuth 类凭据走本机回环代理。
32
72
  3. **试跑一次**:「试跑(不提交)」只抓取与渲染,不写文件、不提交。
33
73
 
34
- > **重新生成(忽略缓存)**:手动「重新生成」会忽略核心持久化的摘要缓存,强制重新抓取与总结,并让榜单页以本次结果为准。若重新生成后内容与分支上已有产物逐字节相同,核心仍按 append-only 语义**不新增提交**(面板提示「内容无变化,未新增提交」);内容有变化才追加一个提交。定时运行永不强制重跑。
35
-
36
- > 设置页分「内容来源 / 模型 / 仓库 / 认证 / 榜单 / 调度」六个 tab;**除「内容来源」(只有状态与核心操作,没有可保存字段)外,每个 tab 独立保存与校验**;某个 tab 有未保存改动时,左侧对应 tab 会显示黄点。
37
-
38
- ## 榜单预览与按周期回看
39
-
40
- - 运行成功后,Host 从核心本次返回的 `outcome.files` 里挑当天的 `.md`(优先 `YYYY-MM-DD.md`,否则只有一个 `.md` 时用它),**只读**复制到 `$DSH_HOME/github-trending/previews/<runId>.md`:路径必须在 `repo.path` 内(词法 + realpath 各判一次)、不超过 512 KB、sha256 与契约声明一致;失败不阻断运行,只是这次没有预览。
41
- - 预览副本 0600、临时文件 + rename、最多保留 90 份;`state.json` / `history.json` 只存 `file` / `bytes` / `sha256` 元数据,不存正文。
42
- - `GET /preview?id=<runId>` 只接受历史里出现过的 id、不接路径参数;返回 `{ text, truncated }`。客户端渲染 Markdown 子集(标题 / 段落 / 列表 / 引用 / 代码块 / 表格(GFM 管道表,含列对齐)/ 粗体 / 行内代码 / 链接 / 分隔线),**只生成 React 文本节点**,链接仅放行 http(s),不渲染图片或原始 HTML;也可以切「原文」。
43
- - 榜单视图按周期分三段(日榜 / 周榜 / 月榜),各周期一条独立时间线:日榜列最近 30 天有记录的日期、每天一条;周榜按 ISO 周、月榜按月。同一期多次运行只保留一条(优先成功且有预览的)。默认打开当前周期最近一期有预览的期数;失败 / 无变更 / 试跑 / 旧记录 / 副本被清理都有各自的空状态,不用示例内容兜底。
44
- - 预览是运行结束时的派生副本:你在仓库里事后编辑 canonical 文件,不会改变这次记录。
45
- - 工作台顶部第一行是插件身份条:名称 / 版本 / 完整包名(点击复制)/ GitHub 外链 / 检查更新(就地状态);第二行是横向 tab(概览 / 榜单 / 运行记录 / 订阅 / 设置),右侧常驻下次运行与就绪 N/4。点版本号可打开「关于」弹层。包名 / 版本 / GitHub 来自 `/state.plugin`(`repository` 由 `package.json` 规范化),仓库缺失时隐藏外链。
46
-
47
- ## 用量(token)
48
-
49
- - **每次运行的 token 总量**与**历史累计总量**都会记录。**只展示总用量,不拆** prompt / completion / cached 等维度。
50
- - 「本次任务总量」= 榜单摘要阶段(`usage.totalTokens`)+ 订阅阶段(`releases.usage.totalTokens`),**订阅消耗计入总量**。
51
- - 累计 = 历史里全部运行 `tokens` 之和,**读时求和、不落盘**(`history.json` 只存每次的 `tokens`)。
52
- - 插件**不做费用估算、不内置定价表**(价格随 provider / 区域变动,易过期):只展示核心回报的 token 真值。
53
- - 展示位置:概览速览块(本次 / 累计 / 新版本 / 订阅仓库数)、运行记录(每行本次 tokens、展开明细、页脚累计)、历史。数据来自契约 `Usage`。旧核心无 `usage` / `releases` 时对应速览块整体隐藏(不铺「—」、不伪造 0)。
54
-
55
- ## 订阅(`releases` 阶段)
56
-
57
- - 订阅一组仓库,之后**每周一次**把每个仓库这一周「**新增的 Release**(无 Release 时回退 Tag)」合成一段中文总结,写到 `<output.dir>/releases/<owner>/<repo>-01.md`(每册都带两位序号,超出行数预算后下一次写入开 `-02.md`,最新一周在最上面),与榜单**同分支、同 PR** 一起提交。订阅阶段**常开**;没有订阅时不产出,既有榜单产物逐字节不变。跨仓的周汇总(`releases/_weekly/<年>/<ISO周>.{md,json}`)继续生成,供外部 Web 服务的 `GET /api/v2/releases*` 消费。
58
- - **订阅列表随数据仓版本化**:文件在 `<repo>/<OutputRoot(output.dir)>/subscriptions.yaml`,随产物同一次 commit / PR,可审计、可复现。
59
- - **插件不直接写仓**:列表经核心 `subscribe` IPC 命令读改——`add` / `remove` 只写 `.git/trendradar-subscriptions.pending`,下一次 `run` 切分支后落盘并随 artifact 提交。插件侧的子进程只做一次 stdin 交互:`RunRequest{command:"subscribe", action, repo?, volume?, configPath}`;核心把 payload 放进结果信封(`subscriptions` / `subscriptionDoc`),旧核心把 `list` 结果逐行打到信封前,插件两种都认。`configPath` 由插件生成(与 `run` 同源的临时配置),保证 `subscribe` 与 `run` 解析到同一个订阅文件;代理模式下 `model.baseURL` 为空,临时配置补一个回环占位端点以通过核心 `config.Load`(订阅命令不调用模型)。
60
- - 界面:「订阅」页管理列表(增删 + 逐仓库状态),可展开某仓库的变更文档(`GET /subscriptions/releases?repo=&volume=`);「设置 → 订阅」分区放行为参数与每周触发节奏。
61
- - 周期:0.7.0 起只支持**每周**(`releases.period=weekly`)——每个 ISO 周、每仓库一条中文摘要,文档按周累积并按行数预算自动分卷;填 `daily`/`monthly` 会被校验拦下并说明原因。
62
- - 每周订阅行程与榜单调度**互相独立**:`releases.schedule`(`enabled` / `weekday`(ISO 1=周一…7=周日)/ `time` / `timezone`)驱动 `run --stages releases`——只跑订阅阶段,不抓榜单、不渲染 digest、不合并订阅 pending;成功后按 ISO 周(如 `2026-W44`)去重。
63
- - 历史里每条运行记录带 `releases` 摘要(仓库数 / 新版本 / 已摘要 / 降级 / 失败 / 跳过)与本次 `tokens`;旧核心没有 `releases` 块时缺省。
64
-
65
-
66
-
67
- - 顶部身份条的「检查更新」与「关于」弹层都会显示当前版本与 npm 上的最新版:首次挂载静默检查一次(Host 侧缓存 5 分钟),也可以点「检查更新」强制重查;有新版时身份条就地显示「有新版本 vX」+「更新到 vX」。只跟随 npm 的 `latest` 稳定版。
68
- - 确认后由宿主 `pluginManager` 重装本插件(`installBundle`;已装同 spec 时走 `removeBundle → installBundle`,失败会尝试装回原版本),完成后需要重启 DSH 生效。
69
- - 本地安装(`link:` / `file:` / `workspace:`)**不提供一键更新**:那会把开发链接换成 npm 版。此时只显示 `dsh plugin --profile <name> add @morehao/dsh-github-trending@latest` 供复制。
70
- - 更新引擎来自共享包 [`@morehao/dsh-plugin-kit`](https://www.npmjs.com/package/@morehao/dsh-plugin-kit)(`./update` + `./update/client`),本仓只做上下文与 `execCapture` 接线;`GET /update` / `POST /update` 与其它路由一样要求自定义头、`no-store`,业务失败返回 409 + `UPDATE_FAILED` + `updateCode`。
71
-
72
- ## 它需要什么 / 它访问哪里 / 它不上传什么
73
-
74
- **需要什么**
75
-
76
- - DSH `0.2.1-rc.3` 以上(`engines.dsh`),Node.js `^22.20.0 || >=24.0.0`(`.nvmrc` 钉 22 线;CI 与发布跑 22)
77
- - 一个本地 **git 仓库**(提交目标)与可写的远端
78
- - 模型:provider + 模型 ID + 密钥(或用宿主已配置的 provider;OAuth 类凭据不需要密钥)
79
- - Git 与 GitHub 凭据(本机 `gh` 登录态 / PAT),仅 push 与开 PR 需要
80
- - 核心二进制:首次运行从 `github.com/morehao/trendradar/releases` 下载(按 `os/arch`,sha256 校验后缓存);之后离线复用。开发/排障时可用 `TRENDING_DIGEST_CORE`(或设置里的「核心路径」)指向一个可执行的 `trendradar`,此时完全不下载
81
-
82
- **访问哪里**
83
-
84
- - 本地核心进程(一次性的 `run` 子进程)
85
- - `github.com/morehao/trendradar/releases`:仅首次运行(或点「下载核心」)时下载二进制与其 `checksums.txt`;之后不再联网
86
- - 你配置的模型端点(由**核心**发起,插件不代发)
87
- - `api.github.com`(由**核心**发起 push/PR;插件本身不发 GitHub 请求)
88
- - 本机 `127.0.0.1` 上的临时回环代理(仅当宿主 provider 的凭据不可导出时,端口随机、一次性 token、运行结束即关闭)
89
-
90
- **不上传什么**
91
-
92
- - 不向作者控制的任何服务器发送数据;插件没有遥测、没有云端账号
93
- - 不读取、不上传与你配置无关的仓库内容;核心只在目标仓库里写 `output.dir` 下的文件;插件唯一的仓库读取是运行成功后按清单复制当天 `.md` 预览副本(见上节),只读、不 commit、不改工作树
94
- - 密钥与 token 只经 `ctx.credentials` 解析、只经运行时通道交给核心:**不写配置文件、不进日志、不进 argv**
95
-
96
- ## 配置
97
-
98
- 设置写在 `$DSH_HOME/github-trending/settings.json`(0600),**不含任何密钥**。
99
-
100
- | 键 | 默认 | 说明 |
101
- |---|---|---|
102
- | `model.provider` | `deepseek` | `deepseek` / `openai-compatible`(以核心 `version --json` 的 `supportedProviders` 为准) |
103
- | `model.mode` | `direct` | `direct` = 密钥直传核心;`proxy` = 本机回环代理转发宿主模型 |
104
- | `model.baseURL` | 空 | 空则用 provider 默认端点;代理模式由插件注入 |
105
- | `model.model` | 空 | 必填,不设隐式默认 |
106
- | `model.apiKeyRef` | `TRENDING_MODEL_KEY` | 指向 `ctx.credentials` 的引用名 |
107
- | `model.temperature` / `maxTokens` | `0.2` / `2048` | 单次调用参数 |
108
- | `model.allowDegraded` | `true` | 模型失败时核心是否退回原文(会标记 degraded) |
109
- | `repo.path` | 空 | 本地仓库绝对路径,必填 |
110
- | `repo.baseBranch` | 读取仓库默认分支 | 只读展示,不允许手改(以 `origin/HEAD` 为准) |
111
- | `repo.remote` | `origin` | 固定 `origin`,界面不展示 |
112
- | `repo.commitMode` | `branch` | `branch`(分支 + PR)/ `direct` |
113
- | `pr.enabled` / `autoMerge` | `true` / `true` | |
114
- | `pr.titleTemplate` | `chore(trending): add {date} digest` | 默认规则,界面不展示 |
115
- | `github.authMode` | `gh` | `gh`(本机 gh 登录态)/ `pat` |
116
- | `github.tokenRef` | `GITHUB_TOKEN` | `ctx.credentials` 引用名(`pat` 用) |
117
- | `trending.since` / `languages` / `topN` | `daily` / `all,typescript,python,go,rust` / `25` | `since` 是单次运行周期(`daily`/`weekly`/`monthly`,抓取窗口=标的周期由核心保证);实际支持范围由核心决定 |
118
- | `summarize.batchSize` / `maxRetries` | `10` / `1` | 摘要批量与失败重试(映射核心 `summarize.*`) |
119
- | `summarize.enrich` | `none` | `none` / `repo` / `readme`:是否抓取 GitHub topics / README 作为增强上下文;`repo`/`readme` 建议配只读 `GITHUB_READ_TOKEN` |
120
- | `summarize.readmeChars` | `800` | README 摘录长度(enrich=readme 时) |
121
- | `releases.includePrerelease` / `includeDrafts` | `false` / `false` | 是否含预发布 / 草稿(订阅阶段常开,不提供 `enabled` 开关) |
122
- | `releases.fallbackToTags` / `baselineOnFirstRun` | `true` / `true` | 无 Release 时回退 Tag / 首次只记基线 |
123
- | `releases.perRepoLimit` / `maxNotesChars` | `10` / `4000` | 文档列出的版本条数(1–20)/ notes 截断(500–20000) |
124
- | `releases.summaryMaxChars` / `docMaxLines` | `1200` / `600` | 每仓每周摘要字数(500–2000)/ 文档行数预算(100–2000,超了开新卷) |
125
- | `releases.concurrency` / `budgetSeconds` | `auto` / `auto` | 订阅并发(`auto` 或 1–8)/ 模型预算秒(`auto` 或 1–300);`auto` 由插件按订阅数解析 |
126
- | `releases.period` | `weekly` | 只支持 `weekly`(每个 ISO 周一条摘要) |
127
- | `releases.schedule.enabled` / `weekday` / `time` / `timezone` | `true` / `1` / `08:00:00` / `Asia/Shanghai` | 每周订阅行程(插件自己的 tick;`weekday` 为 ISO 1=周一…7=周日);**不写进核心 YAML** |
128
- | `output.dir` | `trending` | 仓库内相对目录 |
129
- | `schedule.enabled` / `time` / `timezone` | `true` / `08:30:00` / `Asia/Shanghai` | 插件自己的 60 s tick 驱动 |
130
- | `schedule.periods` | `daily,weekly,monthly` | 定时自动产出的周期:默认三周期全产出(无需勾选),日=每天、周=周日、月=月末触发;旧配置自动升级为三周期 |
131
- | `corePath` | 空 | 等价于 `TRENDING_DIGEST_CORE`(仅开发/排障,界面不提供入口,用环境变量或直接改 `settings.json`;设了就完全不下载) |
132
- | `coreAutoDownload` | `true` | 缓存缺失时是否自动下载核心;关掉后只报 `CORE_NOT_DOWNLOADED`,由你在设置页点「下载核心」 |
133
-
134
- ## 两个工具
135
-
136
- | 工具 | 参数 | 返回 |
137
- |---|---|---|
138
- | `trending_run` | `{ dryRun?: boolean, date?: string, period?: 'daily'\|'weekly'\|'monthly', force?: boolean }` | 结构化摘要:文件数、commit/分支、PR、内容哈希、错误码与下一步 |
139
- | `trending_history` | `{ limit?: number }` | 上次/下次运行、最近 N 条记录、核心版本、模型、未合并 PR |
140
-
141
- 工具名是稳定对外接口:**只新增不改名**。
142
-
143
- ## 错误码
144
-
145
- | 码 | 含义 | 下一步 |
146
- |---|---|---|
147
- | `CORE_NOT_DOWNLOADED` | 核心还没下载(缓存为空,且自动下载被关或尚未触发) | 设置页点「下载核心」,或指向可执行的 `trendradar` |
148
- | `CORE_CHECKSUM_MISMATCH` | 下载内容的 sha256 与 `checksums.txt` 不符(已被丢弃) | 重试;仍失败则手动下载放到缓存目录(代理可能改写内容) |
149
- | `CORE_UNAVAILABLE` | 下载失败 / 版本还没发布(404)/ 平台不在矩阵 / 覆盖路径无效 | 按提示里的 URL 与发布顺序处理 |
150
- | `CORE_INCOMPATIBLE` | 核心 `ipcSchema`/`digestSchema` 与插件不一致 | 升级插件(含平台包)到同一版本 |
151
- | `CORE_TIMEOUT` / `CORE_CANCELLED` | 超时 / 用户取消 | 不提交任何状态,也留不下半成品 |
152
- | `IPC_INVALID` | 核心 stdout 不是约定的结果信封 | 升级插件与核心 |
153
- | `MODEL_KEY_MISSING` / `MODEL_UNAUTHORIZED` / `MODEL_NOT_FOUND` / `MODEL_RATE_LIMITED` | 模型侧 | 设置页「模型」区红字 + 重新选择 |
154
- | `GIT_NOT_INSTALLED` / `REPO_NOT_GIT` / `REPO_DIRTY` | 本机 git / 仓库状态 | 装 git ≥ 2.30、换仓库、先提交改动 |
155
- | `GIT_OPERATION_FAILED` | 核心执行 git 失败(含 push 被拒) | 核心对 `trending/` 日期分支只追加提交、普通 fast-forward 推送;被拒时自动 fetch + rebase 重试一次。若报 `stale info`:在仓库执行 `git fetch --prune origin` 后重试(升级核心后不再出现) |
156
- | `GITHUB_AUTH` / `PUSH_FAILED` / `PR_FAILED` / `NOT_MERGEABLE` | 推送与 PR | 本地 commit 已保留,修好后重试即可复用 |
157
- | `FETCH_FAILED` / `FETCH_RATE_LIMITED` | 抓取 | 检查网络/代理或稍后重试 |
158
-
159
- 核心只在 stderr 给出 `error: <CODE>: ...` 文本;插件优先取核心自己的码,取不到时按稳定文本特征归类,并在结果里标 `inferred: true`(界面会注明「错误码由插件推断」)。
160
-
161
- 运行结果还会带 `skipReason`(`merged` / `pr-closed` / `unchanged`)与 push 失败的 `reason`(`stale-lease` / `non-fast-forward` / `auth` / `protected` / `hook` / `pathspec`)。定时运行(`TRENDRADAR_TRIGGER=schedule`)尊重人工关闭的 PR,不重建分支;手动运行才会重开。
162
-
163
- ## 与本地 CLI 共存
164
-
165
- - 插件**永远使用自带的平台包**,不从 `PATH` 解析核心
166
- - 运行前探测 `PATH` 上的 `trendradar`,版本/协议不一致只**提示**,不切换、不接管它的 cron/launchd
167
- - 首次可在设置页「从本机 CLI 导入」一次性导入非敏感字段;**密钥不导入**
168
- - 别让两套定时同时开着:内容侧靠 `contentHash` 与 PR 复用兜底,模型消耗无法兜底
169
-
170
- ## 实现说明(0.3.0)
171
-
172
- 1. **结果信封**:核心 `run --config <path> --json` 输出契约 v2 `RunResult`(`ipcSchema=2`,可选 `branch/commit/prUrl/contentHash`);插件只解析这一种形状,旧核心的 `runOutput` 不再兼容。
173
- 2. **密钥传输**:POSIX 上配置经 **FIFO** 交给核心(文件系统里不出现明文);Windows 无 FIFO,退化为 0600 临时文件并在读完后立即删除。
174
- 3. **GitHub token** 经子进程环境变量传给核心(核心只认 `GITHUB_TOKEN`/`GH_TOKEN`);不进 argv、不进配置、不进日志。
175
- 4. **调度**用插件自己的 60 s tick(宿主 `schedule` 是会话本地提醒,覆盖不到无人值守场景)。按 `schedule.periods` 分别触发:日榜每天、周榜周日(ISO 周末)、月榜月末;去重键含周期与本地日期,同一周期当天只成功一次。
176
- 5. **二进制改为下载**(2026-10-30 决策 B):不再随包发 npm 平台分包。首次运行需要能访问 GitHub Release;断网环境请用 `TRENDING_DIGEST_CORE` 指向本地核心。核心版本由 `package.json` 的 `dsh.core.version` 固定(用 `node scripts/set-core-version.mjs <x.y.z>` 写入)。
177
- 6. **核心版本固定**:`package.json` 的 `dsh.core.version` 是首次运行下载哪个核心 Release 的唯一来源(用 `node scripts/set-core-version.mjs <x.y.z>` 写入);下载失败会给出下载地址与错误码,不会退回旧内容。
178
- 7. **预览副本是派生数据**:不是第二份真相;仓库里的 canonical 文件才是。旧记录没有 `preview` 元数据,日报里只能看到对应的空状态,不承诺回填。
74
+ 环境要求:DSH `0.2.1-rc.3` 以上、Node.js `^22.20.0 || >=24.0.0`、本地 git ≥ 2.30 与可写远端、git/GitHub 凭据(仅 push 与开 PR 需要)。核心二进制首次运行从 GitHub Release 下载并按 sha256 校验,之后离线复用。
179
75
 
180
76
  ## 开发与发版
181
77
 
182
78
  ```bash
183
79
  npm install
184
- npm run check # typecheck + 契约校验 + 构建 + 全部测试
185
- node scripts/ac/run.mjs # AC-P-01…13 的机械部分
80
+ npm run check # typecheck + 契约校验 + 构建 + 全部测试
81
+ node scripts/ac/run.mjs # AC-P 机械检查
186
82
  ```
187
83
 
188
- 发版走 **tag 触发的 CI**(Trusted Publishing / OIDC,不需要 npm token):合并 PR → 在 `main` 打注记 tag `vX.Y.Z` → CI 发 npm 并用注记正文建 Release。细节见 [docs/publishing.md](docs/publishing.md)。
189
-
190
- - 契约来源:类型与校验器来自 npm 包 `@morehao/trendradar-contract`(`dependencies` 精确锁定;wire `ipcSchema=2` / `digestSchema=2`);本地不再保留 `contract/` 快照与生成器;`npm run check:contract` 校验包版本与 wire 版本
191
- - 核心下载与缓存:`src/core/release.ts`(`ensureCore`),缓存位置 `$DSH_HOME/github-trending/core/v<version>/`
192
- - 宿主半 `src/`(tsup → `lib/`),客户端半 `client/`(esbuild → `client.js`,`window.__ModuleLoader__.load`)
84
+ 发版走 **tag 触发的 CI**(Trusted Publishing / OIDC):合并 PR → 在 `main` 打注记 tag `vX.Y.Z` → CI 发 npm 并用注记正文建 Release。细节见 [docs/publishing.md](docs/publishing.md)。
193
85
 
194
86
  ## 许可
195
87
 
package/client.js CHANGED
@@ -47,7 +47,7 @@ module.exports = __toCommonJS(index_exports);
47
47
  var DICT = {
48
48
  zh: {
49
49
  "section.title": "GitHub 趋势",
50
- "section.subtitle": "每天定时把 GitHub Trending 抓取、总结并提交到你的仓库;内容全部由本地核心产出。",
50
+ "section.subtitle": "按日 / 周 / 月定时抓取、总结 GitHub Trending 并提交到你的仓库;内容全部由本地核心产出。",
51
51
  "tab.settings": "设置",
52
52
  "tab.runs": "运行",
53
53
  "st.source": "内容来源",
@@ -576,7 +576,7 @@ var DICT = {
576
576
  },
577
577
  en: {
578
578
  "section.title": "GitHub Trending",
579
- "section.subtitle": "Fetch, summarize and commit GitHub Trending into your own repo on a daily schedule. All content comes from the local core binary.",
579
+ "section.subtitle": "Fetch, summarize and commit GitHub Trending (daily / weekly / monthly) into your own repo on a schedule. All content comes from the local core binary.",
580
580
  "tab.settings": "Settings",
581
581
  "tab.runs": "Runs",
582
582
  "st.source": "Source",
@@ -1,8 +1,8 @@
1
- import {
2
- __export,
3
- execCapture,
4
- sanitizedEnv
5
- } from "./chunk-R7BK7SJK.js";
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
6
 
7
7
  // src/config.ts
8
8
  var config_exports = {};
@@ -1148,16 +1148,15 @@ __export(handshake_exports, {
1148
1148
  DEFAULT_PROVIDERS: () => DEFAULT_PROVIDERS,
1149
1149
  handshake: () => handshake
1150
1150
  });
1151
+ import { spawnCollect as execCapture } from "@morehao/dsh-plugin-kit/exec";
1151
1152
  var DEFAULT_PROVIDERS = ["deepseek", "openai-compatible"];
1152
1153
  async function handshake(core, options = {}) {
1153
- let result;
1154
- try {
1155
- result = await execCapture(core.path, ["version", "--json"], {
1156
- timeoutMs: options.timeoutMs ?? 5e3,
1157
- signal: options.signal
1158
- });
1159
- } catch (error) {
1160
- const detail = error instanceof Error ? error.message : String(error);
1154
+ const result = await execCapture(core.path, ["version", "--json"], {
1155
+ timeoutMs: options.timeoutMs ?? 5e3,
1156
+ signal: options.signal
1157
+ });
1158
+ if (result.error !== void 0) {
1159
+ const detail = result.error.message;
1161
1160
  throw new IpcError("CORE_SPAWN_FAILED", `\u65E0\u6CD5\u6267\u884C\u6838\u5FC3 ${core.path}\uFF1A${detail}`);
1162
1161
  }
1163
1162
  if (result.aborted || options.signal?.aborted === true) {
@@ -1590,6 +1589,7 @@ __export(run_exports, {
1590
1589
  runCore: () => runCore
1591
1590
  });
1592
1591
  import { spawn as spawn2 } from "child_process";
1592
+ import { sanitizedEnv } from "@morehao/dsh-plugin-kit/exec";
1593
1593
  var DEFAULT_RUN_TIMEOUT_MS = 12e4;
1594
1594
  var MAX_STDOUT = 16 * 1024 * 1024;
1595
1595
  var MAX_STDERR = 2 * 1024 * 1024;
@@ -1772,6 +1772,7 @@ __export(core_exports, {
1772
1772
  });
1773
1773
  import { accessSync as accessSync2, constants as constants3, statSync as statSync4 } from "fs";
1774
1774
  import path7 from "path";
1775
+ import { spawnCollect as execCapture2 } from "@morehao/dsh-plugin-kit/exec";
1775
1776
  function coreRepoFromSource(repo) {
1776
1777
  let ownerRepo = repo;
1777
1778
  if (ownerRepo === void 0) {
@@ -1876,7 +1877,7 @@ async function detectCoexistence(ownPath, options = {}) {
1876
1877
  }
1877
1878
  let status;
1878
1879
  try {
1879
- const result = await execCapture(found, ["version", "--json"], { timeoutMs: 3e3 });
1880
+ const result = await execCapture2(found, ["version", "--json"], { timeoutMs: 3e3 });
1880
1881
  if (result.code !== 0) {
1881
1882
  status = {
1882
1883
  found: true,
@@ -1970,8 +1971,8 @@ async function defaultGhToken(fresh = false) {
1970
1971
  if (!fresh && ghTokenCache !== void 0 && Date.now() - ghTokenCache.at < GH_TOKEN_TTL_MS) {
1971
1972
  return ghTokenCache.value;
1972
1973
  }
1973
- const { execCapture: execCapture2 } = await import("./exec-VMDINNX5.js");
1974
- const result = await execCapture2("gh", ["auth", "token"], { timeoutMs: 2e3 });
1974
+ const { spawnCollect: execCapture6 } = await import("@morehao/dsh-plugin-kit/exec");
1975
+ const result = await execCapture6("gh", ["auth", "token"], { timeoutMs: 2e3 });
1975
1976
  const token = result.code === 0 ? result.stdout.trim() : "";
1976
1977
  const value = token === "" ? void 0 : token;
1977
1978
  ghTokenCache = { value, at: Date.now() };
@@ -2660,6 +2661,7 @@ __export(subscribe_exports, {
2660
2661
  import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
2661
2662
  import os3 from "os";
2662
2663
  import path8 from "path";
2664
+ import { spawnCollect as execCapture3, sanitizedEnv as sanitizedEnv2 } from "@morehao/dsh-plugin-kit/exec";
2663
2665
  var SUBSCRIBE_TIMEOUT_MS = 8e3;
2664
2666
  var RELEASES_TIMEOUT_MS = 3e4;
2665
2667
  var REPO_PATTERN2 = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
@@ -2709,8 +2711,8 @@ async function runSubscribe(input, deps) {
2709
2711
  configPath: file
2710
2712
  };
2711
2713
  const timeoutMs = input.action === "releases" ? RELEASES_TIMEOUT_MS : SUBSCRIBE_TIMEOUT_MS;
2712
- const result = await execCapture(corePath, ["subscribe", "--json"], {
2713
- env: sanitizedEnv(env),
2714
+ const result = await execCapture3(corePath, ["subscribe", "--json"], {
2715
+ env: sanitizedEnv2(env),
2714
2716
  input: JSON.stringify(request),
2715
2717
  timeoutMs
2716
2718
  });
@@ -3411,40 +3413,17 @@ __export(plugin_info_exports, {
3411
3413
  normalizeRepositoryUrl: () => normalizeRepositoryUrl,
3412
3414
  readPluginInfo: () => readPluginInfo
3413
3415
  });
3414
- import { createRequire as createRequire2 } from "module";
3416
+ import { normalizeRepositoryUrl } from "@morehao/dsh-plugin-kit/github";
3417
+ import { createPluginMeta } from "@morehao/dsh-plugin-kit/meta";
3415
3418
  var DEFAULT_NAME = "@morehao/dsh-github-trending";
3416
- function normalizeRepositoryUrl(value) {
3417
- if (typeof value !== "string") return void 0;
3418
- let url = value.trim();
3419
- if (url === "") return void 0;
3420
- url = url.replace(/^git\+/i, "");
3421
- const ssh = /^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/i.exec(url);
3422
- if (ssh !== null) url = `https://github.com/${ssh[1]}/${ssh[2]}`;
3423
- url = url.replace(/\.git$/i, "");
3424
- try {
3425
- const parsed = new URL(url);
3426
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return void 0;
3427
- if (parsed.hostname.toLowerCase() !== "github.com") return void 0;
3428
- const pathname = parsed.pathname.replace(/\/+$/, "");
3429
- if (pathname === "") return void 0;
3430
- return `https://github.com${pathname}`;
3431
- } catch {
3432
- return void 0;
3433
- }
3434
- }
3419
+ var PKG_URL = new URL("../package.json", import.meta.url);
3435
3420
  function readPluginInfo() {
3436
- try {
3437
- const require2 = createRequire2(import.meta.url);
3438
- const pkg = require2("../package.json");
3439
- const repository = typeof pkg.repository === "string" ? normalizeRepositoryUrl(pkg.repository) : typeof pkg.repository === "object" && pkg.repository !== null ? normalizeRepositoryUrl(pkg.repository.url) : void 0;
3440
- return {
3441
- name: typeof pkg.name === "string" && pkg.name !== "" ? pkg.name : DEFAULT_NAME,
3442
- version: typeof pkg.version === "string" ? pkg.version : "0.0.0",
3443
- ...repository === void 0 ? {} : { repository }
3444
- };
3445
- } catch {
3446
- return { name: DEFAULT_NAME, version: "0.0.0" };
3447
- }
3421
+ const meta = createPluginMeta({ pkgUrl: PKG_URL, defaultName: DEFAULT_NAME });
3422
+ return {
3423
+ name: meta.name,
3424
+ version: meta.version,
3425
+ ...meta.repository === void 0 ? {} : { repository: meta.repository }
3426
+ };
3448
3427
  }
3449
3428
 
3450
3429
  // src/host/github.ts
@@ -3958,6 +3937,7 @@ __export(repo_exports, {
3958
3937
  });
3959
3938
  import { statSync as statSync6 } from "fs";
3960
3939
  import path11 from "path";
3940
+ import { spawnCollect as execCapture4 } from "@morehao/dsh-plugin-kit/exec";
3961
3941
  function isDirectory(dir) {
3962
3942
  try {
3963
3943
  return statSync6(dir).isDirectory();
@@ -3967,7 +3947,7 @@ function isDirectory(dir) {
3967
3947
  }
3968
3948
  async function git(dir, args, env) {
3969
3949
  try {
3970
- const result = await execCapture("git", ["-C", dir, ...args], { env, timeoutMs: 5e3 });
3950
+ const result = await execCapture4("git", ["-C", dir, ...args], { env, timeoutMs: 5e3 });
3971
3951
  return result.code === 0 ? result.stdout.trim() : void 0;
3972
3952
  } catch {
3973
3953
  return void 0;
@@ -3998,77 +3978,53 @@ __export(routes_exports, {
3998
3978
  ROUTE_PREFIX: () => ROUTE_PREFIX,
3999
3979
  createRoutes: () => createRoutes
4000
3980
  });
3981
+ import { createRouteHandler, HttpError } from "@morehao/dsh-plugin-kit/http";
4001
3982
  import { PluginUpdateError } from "@morehao/dsh-plugin-kit/update";
4002
3983
  var ROUTE_PREFIX = "/api/morehao/github-trending";
4003
3984
  var HEADER = "x-dsh-github-trending";
4004
3985
  var MAX_BODY2 = 512 * 1024;
4005
- function sendJson2(response, status, payload) {
4006
- const body = JSON.stringify(payload);
4007
- response.writeHead(status, {
4008
- "content-type": "application/json; charset=utf-8",
4009
- "cache-control": "no-store",
4010
- "content-length": Buffer.byteLength(body)
4011
- });
4012
- response.end(body);
4013
- }
4014
- async function readBody2(request) {
4015
- const chunks = [];
4016
- let size = 0;
4017
- for await (const chunk of request) {
4018
- size += chunk.length;
4019
- if (size > MAX_BODY2) throw new IpcError("PAYLOAD_TOO_LARGE", "\u8BF7\u6C42\u4F53\u8FC7\u5927");
4020
- chunks.push(chunk);
4021
- }
4022
- if (chunks.length === 0) return {};
4023
- const text = Buffer.concat(chunks).toString("utf8").trim();
4024
- if (text === "") return {};
4025
- try {
4026
- const value = JSON.parse(text);
4027
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
4028
- throw new Error("body must be a JSON object");
4029
- }
4030
- return value;
4031
- } catch (error) {
4032
- throw new IpcError("CONFIG_INVALID", `\u8BF7\u6C42\u4F53\u4E0D\u662F JSON \u5BF9\u8C61\uFF1A${error instanceof Error ? error.message : String(error)}`);
4033
- }
4034
- }
4035
3986
  function createRoutes(deps) {
4036
- return async function handler(request, response) {
4037
- if (request.headers[HEADER] !== "1") {
4038
- sendJson2(response, 403, { ok: false, code: "FORBIDDEN", message: `\u7F3A\u5C11 ${HEADER}: 1 \u5934` });
4039
- return;
4040
- }
4041
- const url = new URL(request.url ?? "/", "http://127.0.0.1");
4042
- const pathname = url.pathname.startsWith(ROUTE_PREFIX) ? url.pathname.slice(ROUTE_PREFIX.length) || "/" : url.pathname;
4043
- const method = (request.method ?? "GET").toUpperCase();
4044
- try {
4045
- const result = await dispatch(method, pathname, url, request, deps);
4046
- if (!response.writableEnded) sendJson2(response, 200, { ok: true, ...result });
4047
- } catch (error) {
4048
- if (response.writableEnded) return;
3987
+ const logger = deps.logger;
3988
+ let current = { method: "GET", pathname: "/" };
3989
+ const handler = createRouteHandler({
3990
+ prefix: ROUTE_PREFIX,
3991
+ header: HEADER,
3992
+ maxBodyBytes: MAX_BODY2,
3993
+ dispatch: async ({ method, pathname, url, body }) => {
3994
+ current = { method, pathname };
3995
+ return dispatch(method, pathname, url, body, deps);
3996
+ },
3997
+ // 错误码翻译留在插件侧(dispatcher 不做映射):保持与原实现的响应形状逐字一致。
3998
+ onError: (error) => {
4049
3999
  if (error instanceof PluginUpdateError) {
4050
- deps.logger?.("update.failed", { code: error.code, message: error.message });
4051
- sendJson2(response, 409, {
4052
- ok: false,
4000
+ logger?.("update.failed", { code: error.code, message: error.message });
4001
+ return {
4002
+ status: 409,
4053
4003
  code: "UPDATE_FAILED",
4054
- updateCode: error.code,
4055
4004
  message: redact(error.message),
4056
- update: deps.update?.view() ?? null
4057
- });
4058
- return;
4005
+ extra: { updateCode: error.code, update: deps.update?.view() ?? null }
4006
+ };
4007
+ }
4008
+ if (error instanceof HttpError) {
4009
+ const code2 = error.code === "PAYLOAD_TOO_LARGE" ? "PAYLOAD_TOO_LARGE" : "CONFIG_INVALID";
4010
+ return { status: 400, code: code2, message: redact(error.message) };
4059
4011
  }
4060
4012
  const code = error instanceof IpcError ? error.code : "INTERNAL";
4061
4013
  const message = redact(error instanceof Error ? error.message : String(error));
4062
- deps.logger?.("route.error", { pathname, method, code, message });
4014
+ logger?.("route.error", { pathname: current.pathname, method: current.method, code, message });
4063
4015
  const status = code === "CONFIG_INVALID" ? (
4064
4016
  // 「已有一次运行在进行中」是并发冲突而非参数错误:用 409,前端据此禁用按钮并提示。
4065
4017
  message.includes("\u8FD0\u884C\u5728\u8FDB\u884C\u4E2D") ? 409 : 400
4066
4018
  ) : code === "NOT_FOUND" ? 404 : 500;
4067
- sendJson2(response, status, { ok: false, code, message });
4068
- }
4019
+ return { status, code, message };
4020
+ },
4021
+ logger
4022
+ });
4023
+ return async function routeHandler(request, response) {
4024
+ await handler(request, response);
4069
4025
  };
4070
4026
  }
4071
- async function dispatch(method, pathname, url, request, deps) {
4027
+ async function dispatch(method, pathname, url, body, deps) {
4072
4028
  const env = deps.env ?? process.env;
4073
4029
  const settings = deps.store.readSettings();
4074
4030
  if (method === "GET" && pathname === "/state") return statePayload(deps, settings);
@@ -4079,7 +4035,6 @@ async function dispatch(method, pathname, url, request, deps) {
4079
4035
  }
4080
4036
  if (method === "POST" && pathname === "/update") {
4081
4037
  if (deps.update === void 0) throw new IpcError("NOT_FOUND", "\u81EA\u66F4\u65B0\u670D\u52A1\u4E0D\u53EF\u7528");
4082
- const body = await readBody2(request);
4083
4038
  const version = typeof body.version === "string" && body.version.trim() !== "" ? body.version.trim() : void 0;
4084
4039
  const result = await deps.update.apply(version === void 0 ? {} : { version });
4085
4040
  return { result, update: deps.update.view() };
@@ -4095,7 +4050,6 @@ async function dispatch(method, pathname, url, request, deps) {
4095
4050
  return { downloaded: true, path: core.path, tag: core.tag ?? null, core: status };
4096
4051
  }
4097
4052
  if (method === "POST" && pathname === "/config") {
4098
- const body = await readBody2(request);
4099
4053
  const incoming = normalizeSettings(body.settings ?? body);
4100
4054
  const section = typeof body.section === "string" && SETTINGS_SECTIONS.includes(body.section) ? body.section : void 0;
4101
4055
  let next = section === void 0 ? incoming : mergeSettingsSection(settings, incoming, section);
@@ -4114,7 +4068,6 @@ async function dispatch(method, pathname, url, request, deps) {
4114
4068
  return { saved: true, settings: next, problems: [] };
4115
4069
  }
4116
4070
  if (method === "POST" && pathname === "/preflight") {
4117
- const body = await readBody2(request);
4118
4071
  const candidate = body.settings === void 0 ? settings : normalizeSettings(body.settings);
4119
4072
  const result = await preflightModel({
4120
4073
  settings: candidate,
@@ -4125,7 +4078,6 @@ async function dispatch(method, pathname, url, request, deps) {
4125
4078
  return { preflight: result };
4126
4079
  }
4127
4080
  if (method === "POST" && pathname === "/run") {
4128
- const body = await readBody2(request);
4129
4081
  const base = {
4130
4082
  dryRun: body.dryRun === true,
4131
4083
  ...typeof body.date === "string" && body.date !== "" ? { date: body.date } : {},
@@ -4143,7 +4095,6 @@ async function dispatch(method, pathname, url, request, deps) {
4143
4095
  return { cancelled: deps.runner.cancel() };
4144
4096
  }
4145
4097
  if (method === "POST" && pathname === "/releases/run") {
4146
- const body = await readBody2(request);
4147
4098
  let repo;
4148
4099
  if (body.repo !== void 0) {
4149
4100
  if (typeof body.repo !== "string" || !isValidRepo(body.repo.trim())) {
@@ -4191,7 +4142,6 @@ async function dispatch(method, pathname, url, request, deps) {
4191
4142
  return { ...result.doc };
4192
4143
  }
4193
4144
  if (method === "POST" && pathname === "/subscriptions") {
4194
- const body = await readBody2(request);
4195
4145
  const action = body.action === "add" || body.action === "remove" ? body.action : void 0;
4196
4146
  if (action === void 0) throw new IpcError("CONFIG_INVALID", "action \u5FC5\u987B\u662F add \u6216 remove");
4197
4147
  const repos = Array.isArray(body.repos) ? body.repos.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()) : typeof body.repo === "string" && body.repo.trim() !== "" ? [body.repo.trim()] : [];
@@ -4206,7 +4156,6 @@ async function dispatch(method, pathname, url, request, deps) {
4206
4156
  return { repos: latest.ok ? latest.repos : [], items: latest.ok ? latest.items ?? [] : [] };
4207
4157
  }
4208
4158
  if (method === "POST" && pathname === "/github/test") {
4209
- const body = await readBody2(request);
4210
4159
  const candidate = body.settings === void 0 ? settings : normalizeSettings(body.settings);
4211
4160
  const probe = await probeGithubAuth({
4212
4161
  credentials: credentialServices(deps.get),
@@ -4225,7 +4174,6 @@ async function dispatch(method, pathname, url, request, deps) {
4225
4174
  return { providers: candidates, defaultSelection: readDefaultSelection(deps.get) ?? null };
4226
4175
  }
4227
4176
  if (method === "POST" && pathname === "/quickfill") {
4228
- const body = await readBody2(request);
4229
4177
  const candidates = await listProviderCandidates({
4230
4178
  llm: llmServices(deps.get),
4231
4179
  credentials: credentialServices(deps.get),
@@ -4357,6 +4305,7 @@ __export(update_exports, {
4357
4305
  });
4358
4306
  import { normalizeProfile } from "@morehao/dsh-plugin-kit";
4359
4307
  import { createPluginUpdater } from "@morehao/dsh-plugin-kit/update";
4308
+ import { spawnCollect as execCapture5, sanitizedEnv as sanitizedEnv3 } from "@morehao/dsh-plugin-kit/exec";
4360
4309
  function pnpmViewRunner(get, env, exec) {
4361
4310
  return async (spec) => {
4362
4311
  const profile = normalizeProfile(get("profileContext"));
@@ -4367,7 +4316,7 @@ function pnpmViewRunner(get, env, exec) {
4367
4316
  try {
4368
4317
  result = await exec(command, [...prefix, "view", spec, "version", "--json"], {
4369
4318
  cwd: profile.dir,
4370
- env: sanitizedEnv(env),
4319
+ env: sanitizedEnv3(env),
4371
4320
  timeoutMs: 8e3,
4372
4321
  maxStdout: 64 * 1024,
4373
4322
  maxStderr: 16 * 1024
@@ -4389,7 +4338,7 @@ function pnpmViewRunner(get, env, exec) {
4389
4338
  };
4390
4339
  }
4391
4340
  function createUpdateService(deps) {
4392
- const exec = deps.exec ?? execCapture;
4341
+ const exec = deps.exec ?? execCapture5;
4393
4342
  return createPluginUpdater({
4394
4343
  meta: deps.meta,
4395
4344
  getPluginManager: () => deps.get("pluginManager"),
package/lib/index.js CHANGED
@@ -8,8 +8,7 @@ import {
8
8
  createTrendingRunTool,
9
9
  createUpdateService,
10
10
  readPluginInfo
11
- } from "./chunk-HYVMRVGP.js";
12
- import "./chunk-R7BK7SJK.js";
11
+ } from "./chunk-YJ5GF3MW.js";
13
12
 
14
13
  // src/index.ts
15
14
  var name = "dsh-github-trending";
package/lib/internal.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { ErrorCode, FileArtifact, Usage, ReleasesReport, SubscriptionItem, SubscriptionDoc, SubscriptionStatus, ErrorInfo, RunRequest, RunResult, State, StateRun, SubscriptionList, SubscriptionVolume } from '@morehao/trendradar-contract';
2
+ import { ExecOptions, ExecResult } from '@morehao/dsh-plugin-kit/exec';
3
+ import * as exec from '@morehao/dsh-plugin-kit/exec';
4
+ export { exec };
5
+ import { normalizeRepositoryUrl } from '@morehao/dsh-plugin-kit/github';
2
6
  import http from 'node:http';
3
7
  import { Updater, PluginMeta, Logger } from '@morehao/dsh-plugin-kit/update';
4
8
 
@@ -930,40 +934,6 @@ declare namespace channel {
930
934
  export { type channel_ChannelKind as ChannelKind, type channel_ConfigChannel as ConfigChannel, channel_WRITE_TIMEOUT_MS as WRITE_TIMEOUT_MS, channel_openConfigChannel as openConfigChannel, channel_removeChannelDir as removeChannelDir };
931
935
  }
932
936
 
933
- interface ExecResult {
934
- code: number | null;
935
- signal: NodeJS.Signals | null;
936
- stdout: string;
937
- stderr: string;
938
- timedOut: boolean;
939
- aborted: boolean;
940
- durationMs: number;
941
- }
942
- declare function sanitizedEnv(source?: NodeJS.ProcessEnv, extra?: Record<string, string | undefined>): Record<string, string>;
943
- interface ExecOptions {
944
- env?: NodeJS.ProcessEnv;
945
- cwd?: string;
946
- timeoutMs?: number;
947
- signal?: AbortSignal;
948
- maxStdout?: number;
949
- maxStderr?: number;
950
- /** 写入子进程 stdin 的内容(写完即关闭);省略则 stdin 为 ignore。 */
951
- input?: string;
952
- }
953
- /**
954
- * 执行一次短命令并收集输出(握手、共存探测都走这里)。
955
- * 超时与取消都先 SIGTERM、3 s 后 SIGKILL——核心是单进程,不需要 process group。
956
- */
957
- declare function execCapture(file: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
958
-
959
- type exec_ExecOptions = ExecOptions;
960
- type exec_ExecResult = ExecResult;
961
- declare const exec_execCapture: typeof execCapture;
962
- declare const exec_sanitizedEnv: typeof sanitizedEnv;
963
- declare namespace exec {
964
- export { type exec_ExecOptions as ExecOptions, type exec_ExecResult as ExecResult, exec_execCapture as execCapture, exec_sanitizedEnv as sanitizedEnv };
965
- }
966
-
967
937
  declare const DEFAULT_RUN_TIMEOUT_MS = 120000;
968
938
  interface RunCoreOptions {
969
939
  core: CoreLocation;
@@ -1654,13 +1624,24 @@ declare namespace subscribe {
1654
1624
  export { subscribe_RELEASES_TIMEOUT_MS as RELEASES_TIMEOUT_MS, subscribe_SUBSCRIBE_TIMEOUT_MS as SUBSCRIBE_TIMEOUT_MS, type subscribe_SubscribeDeps as SubscribeDeps, subscribe_isValidRepo as isValidRepo, subscribe_runSubscribe as runSubscribe };
1655
1625
  }
1656
1626
 
1627
+ /**
1628
+ * 插件自身元数据:包名、版本、仓库地址。
1629
+ *
1630
+ * `/state.plugin` 是界面上「关于」的唯一来源:版本不能硬编码在客户端,仓库地址
1631
+ * 也不能从客户端猜。`package.json.repository.url` 常见形态是
1632
+ * `git+https://github.com/o/r.git` 或 `git@github.com:o/r.git`,统一规范化成
1633
+ * `https://github.com/o/r`;解析不了就返回 undefined,界面隐藏外链而不是猜。
1634
+ *
1635
+ * 读包与规范化仓库地址都交给 `@morehao/dsh-plugin-kit` 的 `./meta` + `./github`;
1636
+ * 这里保留 `readPluginInfo()` / `normalizeRepositoryUrl()` 两个对内部(测试与 index)
1637
+ * 的既有导出面,形状不变。
1638
+ */
1639
+
1657
1640
  interface PluginInfo {
1658
1641
  name: string;
1659
1642
  version: string;
1660
1643
  repository?: string;
1661
1644
  }
1662
- /** 只放行 GitHub 的 http(s) 地址;ssh / git+ 前缀先归一化。 */
1663
- declare function normalizeRepositoryUrl(value: unknown): string | undefined;
1664
1645
  declare function readPluginInfo(): PluginInfo;
1665
1646
 
1666
1647
  type pluginInfo_PluginInfo = PluginInfo;
@@ -2164,4 +2145,4 @@ declare namespace tools {
2164
2145
  export { tools_createTrendingHistoryTool as createTrendingHistoryTool, tools_createTrendingRunTool as createTrendingRunTool };
2165
2146
  }
2166
2147
 
2167
- export { channel, config, core, credentials, errors, exec, github, handshake$1 as handshake, importCli, ipc, locate, paths, pendingPr, pluginInfo, preflight, proxy, quickfill, release, repo, routes, run, runner, schedule, state, subscribe, tools, update as updateService, version };
2148
+ export { channel, config, core, credentials, errors, github, handshake$1 as handshake, importCli, ipc, locate, paths, pendingPr, pluginInfo, preflight, proxy, quickfill, release, repo, routes, run, runner, schedule, state, subscribe, tools, update as updateService, version };
package/lib/internal.js CHANGED
@@ -26,17 +26,17 @@ import {
26
26
  tools_exports,
27
27
  update_exports,
28
28
  version_exports
29
- } from "./chunk-HYVMRVGP.js";
30
- import {
31
- exec_exports
32
- } from "./chunk-R7BK7SJK.js";
29
+ } from "./chunk-YJ5GF3MW.js";
30
+
31
+ // src/internal.ts
32
+ import * as exec from "@morehao/dsh-plugin-kit/exec";
33
33
  export {
34
34
  channel_exports as channel,
35
35
  config_exports as config,
36
36
  core_exports as core,
37
37
  credentials_exports as credentials,
38
38
  errors_exports as errors,
39
- exec_exports as exec,
39
+ exec,
40
40
  github_exports as github,
41
41
  handshake_exports as handshake,
42
42
  import_cli_exports as importCli,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morehao/dsh-github-trending",
3
- "version": "0.7.8",
4
- "description": "GitHub 趋势日报插件:每天定时 spawn 本地 trendradar 核心,抓取 Trending、生成中文简介与总结,提交到你的仓库并自动合并 PR。",
3
+ "version": "0.7.10",
4
+ "description": "GitHub 趋势日报的 DeepSeek Harness 插件:按日 / 周 / 月周期定时 spawn 本地 trendradar 核心,抓取 Trending、生成中文简介与周期总结,并可选把订阅仓库的新 Release 合成中文摘要,一起提交到你的仓库、自动开 PR 合并;内置概览 / 榜单 / 运行记录 / 订阅 / 设置工作台。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -39,10 +39,10 @@
39
39
  ],
40
40
  "engines": {
41
41
  "node": "^22.20.0 || >=24.0.0",
42
- "dsh": ">=0.2.1-rc.3 <0.3.0-0"
42
+ "dsh": ">=0.2.1-rc.3 <0.3.0-0 || >=0.2.2-alpha.0 <0.3.0-0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@deepseek-ai/dsh-tools": ">=0.2.1-rc.3 <0.3.0-0"
45
+ "@deepseek-ai/dsh-tools": ">=0.2.1-rc.3 <0.3.0-0 || >=0.2.2-alpha.0 <0.3.0-0"
46
46
  },
47
47
  "peerDependenciesMeta": {
48
48
  "@deepseek-ai/dsh-tools": {
@@ -50,7 +50,7 @@
50
50
  }
51
51
  },
52
52
  "dependencies": {
53
- "@morehao/dsh-plugin-kit": "0.2.1",
53
+ "@morehao/dsh-plugin-kit": "^0.3.1",
54
54
  "@morehao/trendradar-contract": "0.5.0"
55
55
  },
56
56
  "dsh": {
@@ -1,121 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
5
- };
6
-
7
- // src/core/exec.ts
8
- var exec_exports = {};
9
- __export(exec_exports, {
10
- execCapture: () => execCapture,
11
- sanitizedEnv: () => sanitizedEnv
12
- });
13
- import { spawn } from "child_process";
14
- var ENV_KEYS = [
15
- "PATH",
16
- "HOME",
17
- "USER",
18
- "LOGNAME",
19
- "SHELL",
20
- "TMPDIR",
21
- "TMP",
22
- "TEMP",
23
- "LANG",
24
- "LC_ALL",
25
- "LC_CTYPE",
26
- "SSL_CERT_FILE",
27
- "SSL_CERT_DIR",
28
- "GIT_AUTHOR_NAME",
29
- "GIT_AUTHOR_EMAIL",
30
- "GIT_COMMITTER_NAME",
31
- "GIT_COMMITTER_EMAIL",
32
- "GIT_CONFIG_GLOBAL",
33
- "GIT_SSH_COMMAND"
34
- ];
35
- var PROXY_KEYS = [
36
- "HTTP_PROXY",
37
- "HTTPS_PROXY",
38
- "NO_PROXY",
39
- "ALL_PROXY",
40
- "http_proxy",
41
- "https_proxy",
42
- "no_proxy",
43
- "all_proxy"
44
- ];
45
- function sanitizedEnv(source = process.env, extra = {}) {
46
- const env = {};
47
- for (const key of [...ENV_KEYS, ...PROXY_KEYS]) {
48
- const value = source[key];
49
- if (typeof value === "string" && value !== "") env[key] = value;
50
- }
51
- for (const [key, value] of Object.entries(extra)) {
52
- if (typeof value === "string" && value !== "") env[key] = value;
53
- }
54
- return env;
55
- }
56
- function killTree(child, signal) {
57
- try {
58
- child.kill(signal);
59
- } catch {
60
- }
61
- }
62
- async function execCapture(file, args, options = {}) {
63
- const started = Date.now();
64
- const timeoutMs = options.timeoutMs ?? 1e4;
65
- const maxStdout = options.maxStdout ?? 1024 * 1024;
66
- const maxStderr = options.maxStderr ?? 1024 * 1024;
67
- const child = spawn(file, args, {
68
- cwd: options.cwd,
69
- env: options.env ?? process.env,
70
- stdio: [options.input === void 0 ? "ignore" : "pipe", "pipe", "pipe"],
71
- windowsHide: true
72
- });
73
- if (options.input !== void 0) {
74
- child.stdin?.on("error", () => void 0);
75
- child.stdin?.end(options.input);
76
- }
77
- let stdout = "";
78
- let stderr = "";
79
- let timedOut = false;
80
- let aborted = false;
81
- const hardKill = setTimeout(() => killTree(child, "SIGKILL"), timeoutMs + 3e3);
82
- const timer = setTimeout(() => {
83
- timedOut = true;
84
- killTree(child, "SIGTERM");
85
- }, timeoutMs);
86
- const onAbort = () => {
87
- aborted = true;
88
- killTree(child, "SIGTERM");
89
- };
90
- options.signal?.addEventListener("abort", onAbort, { once: true });
91
- child.stdout?.setEncoding("utf8");
92
- child.stdout?.on("data", (chunk) => {
93
- if (stdout.length < maxStdout) stdout += chunk;
94
- });
95
- child.stderr?.setEncoding("utf8");
96
- child.stderr?.on("data", (chunk) => {
97
- if (stderr.length < maxStderr) stderr += chunk;
98
- });
99
- const result = await new Promise((resolve, reject) => {
100
- child.on("error", (error) => reject(error));
101
- child.on("close", (code, signal) => resolve({ code, signal }));
102
- });
103
- clearTimeout(timer);
104
- clearTimeout(hardKill);
105
- options.signal?.removeEventListener("abort", onAbort);
106
- return {
107
- ...result,
108
- stdout,
109
- stderr,
110
- timedOut,
111
- aborted,
112
- durationMs: Date.now() - started
113
- };
114
- }
115
-
116
- export {
117
- __export,
118
- sanitizedEnv,
119
- execCapture,
120
- exec_exports
121
- };
@@ -1,8 +0,0 @@
1
- import {
2
- execCapture,
3
- sanitizedEnv
4
- } from "./chunk-R7BK7SJK.js";
5
- export {
6
- execCapture,
7
- sanitizedEnv
8
- };