source-code-mgmt 1.13.0 → 1.14.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.en.md +15 -10
- package/README.md +15 -10
- package/lib/client.js +11 -207
- package/lib/index.js +7 -684
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# source-code-mgmt — DSH Source Code Management Plugin
|
|
2
2
|
|
|
3
|
-
> Version: **v1.
|
|
3
|
+
> Version: **v1.14.0** | 中文版见 [README.md](README.md)
|
|
4
4
|
|
|
5
5
|
> A source-code management plugin for the DSH Web GUI: it bundles「environment check → SSH setup → commit/push/upload code」into one「Code Management」panel with GitHub / Gitee dual-platform support.
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ Entry points (auto-detected, no manual switching):
|
|
|
19
19
|
|
|
20
20
|
> Detection is a single in-memory read at activation time (`ctx.get('betterSidebar')`) — zero I/O, zero network, no impact on DSH startup.
|
|
21
21
|
|
|
22
|
-
The panel has
|
|
22
|
+
The panel has five steps (①②③ are the core three; ④⑤ are the newer extensions, collapsed by default):
|
|
23
23
|
|
|
24
24
|
### ① Environment Check
|
|
25
25
|
- Shows the **OS** (friendly names `Windows` / `macOS` / `Linux`, from the underlying `win32` / `darwin` / `linux` platform ids)
|
|
@@ -59,13 +59,7 @@ The panel has six steps (①②③ are the core three; ④⑤⑥ are the newer e
|
|
|
59
59
|
- **Clone into**: optional target parent directory (default = DSH's default workspace; leave empty to clone to the default location); cloning uses the SSH URL and auto-adds the clone to the custom-directory list so ③ can select it directly
|
|
60
60
|
- Repos already present locally can't be re-cloned; every other repo has a「克隆」(clone) button
|
|
61
61
|
|
|
62
|
-
### ⑤
|
|
63
|
-
- **Target repo recorded locally**: paste the repo URL you want to PR to (`https://github.com/owner/repo`, `owner/repo`, …) → saved to `~/.dsh/storages/source-code-mgmt-pr-targets.json` (**not** the plugin directory); saved repos appear as removable chips and can be re-selected
|
|
64
|
-
- **AI PR-rule analysis**: fetches the target repo's README / CONTRIBUTING / PR template / package.json scripts and has the **DSH default model** figure out how to PR to that repo (e.g. awesome-dsh-plugin: create `data/plugins/<owner>__<repo>.yml`, regenerate the README, satisfy the `dsh.bundle`/commit-count/topic gates), producing a structured **step rule cached in the plugin directory `rules/<owner>__<repo>.json`** — the next PR to the same repo runs straight from cache, **no repeated AI spend**;「Re-analyze (ignore cache)」forces a fresh pass
|
|
65
|
-
- **AI PR-content generation**: fill in the plugin info (owner/repo, category, en/zh description) → AI builds the title + description + entry-file content from the rule template — **fully editable, never submitted directly**
|
|
66
|
-
- **Run PR**: executes the rule steps `fork → clone fork → add upstream → fetch → create branch → write entry → (regenerate README) → commit → push → create PR` (GitHub via `gh pr create`, Gitee via OpenAPI) with per-step logs and the PR link; working dir defaults to the default workspace
|
|
67
|
-
|
|
68
|
-
### ⑥ Publish npm package
|
|
62
|
+
### ⑤ Publish npm package
|
|
69
63
|
- Five-step wizard (run in the target directory): **① check registry** `npm config get registry` → **② view auth config** `npm config list` (auto-redacts token/auth/password) → **③ verify identity** `npm whoami` → **④ preview packed files** `npm pack --dry-run` (see exactly what would be published, nothing is packed or uploaded) → **⑤ publish** `npm publish`
|
|
70
64
|
- Publishing requires ticking「I've reviewed the above — confirm publishing to the npm registry」first, preventing accidental publishes
|
|
71
65
|
|
|
@@ -212,7 +206,18 @@ dsh plugin --profile web add link:$(pwd)
|
|
|
212
206
|
|
|
213
207
|
## Version history
|
|
214
208
|
|
|
215
|
-
### v1.
|
|
209
|
+
### v1.14.0 (current)
|
|
210
|
+
Per your request the **⑤ "Submit PR" feature has been removed**:
|
|
211
|
+
|
|
212
|
+
- **Front-end**: `lib/client.js` drops the whole `PrSection` (its render in the panel and the "⑤ Submit PR" mention in the panel intro), along with the now-unused `EN_DICT` PR keys.
|
|
213
|
+
- **Host**: `lib/index.js` removes the entire PR implementation — the `/pr/targets`, `/pr/rule`, `/pr/analyze`, `/pr/generate`, `/pr/execute` routes; `prAnalyzeFlow` / `prGenerateFlow` / `prExecuteFlow`; the PR-targets local store (`source-code-mgmt-pr-targets.json`); the PR-rule cache read/write under `rules/` (`readPrRule`/`writePrRule`, etc.); `parseRepoUrl`; `fillPrTemplate`; the safety checks (`safeEntryPath`/`safeRegenerateCommand`); `PRESET_PR_RULES` and the startup rule-seeding call.
|
|
214
|
+
- `llmComplete()` (the generic LLM helper) is kept — it is independent of PR and reusable.
|
|
215
|
+
- Version bumped 1.13.0 → 1.14.0; `package.json` description now drops "AI-assisted Pull Requests". The clone section (by-URL clone, local-exists detection) and ⑥ npm (empty-by-default dir) keep their v1.13.0 behavior unchanged.
|
|
216
|
+
- Scope: `lib/client.js` (refresh to apply), `lib/index.js` (restart dsh web).
|
|
217
|
+
|
|
218
|
+
> Note: the old `rules/awesome-dsh-plugin__awesome-dsh-plugin.json` preset is no longer referenced by any code — you may delete it or leave it (it doesn't affect anything).
|
|
219
|
+
|
|
220
|
+
### v1.13.0 (history)
|
|
216
221
|
Second feedback pass (④⑤⑥ interaction tightening):
|
|
217
222
|
|
|
218
223
|
- **④ "Already local" detection widened to registered dirs**: previously the clone list only checked under the currently-selected clone dir; if you switched the clone target elsewhere (e.g. the `workspace` dir) where a repo already existed, it still showed "cloneable". Now a new `localRepoExistsAnywhere()` checks the selected dir **plus every workspace / favorite dir registered by step ③** (from `~/.dsh/storages/workspace.json` + `source-code-mgmt-dirs.json`) — a repo present in any registered location is marked "Already local". Since clone success already records the destination into favorite dirs, "this location already has the repo" knowledge persists automatically and the list hits on it next time.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> **界面语言跟随 DSH 设置实时切换**:面板与 host 端消息自动使用 DSH 的语言(设置 → 通用 → 语言),中文 ↔ 英文即时生效,无需重启。
|
|
8
8
|
|
|
9
|
-
> DSH Web GUI 源代码管理插件:把「环境检查 → SSH 配置 → 代码上传推送 → 克隆仓库 →
|
|
9
|
+
> DSH Web GUI 源代码管理插件:把「环境检查 → SSH 配置 → 代码上传推送 → 克隆仓库 → 发布 npm 包」整合进「代码管理」面板,支持 GitHub / Gitee 双平台,一键管理代码仓库。
|
|
10
10
|
|
|
11
11
|
> 入口位置自适应:**已安装 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 时**,「代码管理」作为它侧边栏的一个新 Tab 页面出现(全新侧边栏 Tab);**未安装时**,「代码管理」按钮位于 DSH **右上角、常驻可见**——有会话时放在「Session 日志」旁边的右对齐列表里,无会话空态时改为一个固定在**右上角**的浮动按钮,点击后打开一个 **dsh-better-sidebar 外观的右侧集成面板**(推挤主内容区)。两种形态都复用同一套面板 UI。
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
> 检测只是激活时一次内存读取(`ctx.get('betterSidebar')`),零 I/O、零网络,不影响 DSH 启动速度;两种形态间自动切换。未安装 better-sidebar 时入口常驻右上角(有会话=Session 日志旁,空态=固定右上角浮动按钮),空态/新对话也可见。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
面板分五步(①②③ 为核心三步,④⑤ 为新扩展板块,默认折叠按需展开):
|
|
23
23
|
|
|
24
24
|
### ① 环境检查
|
|
25
25
|
- 显示**操作系统**(美化名:`Windows` / `macOS` / `Linux`,对应底层 Node 平台标识 `win32` / `darwin` / `linux`)
|
|
@@ -65,13 +65,7 @@
|
|
|
65
65
|
- **克隆到目录**:可选目标父目录(默认 = DSH 默认工作区),不选即克隆到默认位置;克隆走 SSH URL(`git@github.com:…` / `git@gitee.com:…`),成功后自动加入自定义目录列表,③ 里可直接选中
|
|
66
66
|
- 列表里「本地已有」的仓库不可重复克隆,其余每个仓库一个「克隆」按钮
|
|
67
67
|
|
|
68
|
-
### ⑤
|
|
69
|
-
- **目标仓库记录到本地**:填写要提交 PR 的仓库网址(`https://github.com/owner/repo` / `owner/repo` 等)→ 记录到 `~/.dsh/storages/source-code-mgmt-pr-targets.json`(**不写入插件目录**);已记录仓库以标签展示、点选回填、可删除
|
|
70
|
-
- **AI 分析 PR 规则**:抓取目标仓库的 README / CONTRIBUTING / PR 模板 / package.json scripts,交给 DSH 默认模型**思考该仓库应如何 PR**(如 awesome-dsh-plugin 收录插件:新建 `data/plugins/<owner>__<repo>.yml`、重生成 README、满足 `dsh.bundle`/提交数/topic 等门槛),输出结构化的**步骤规则并缓存到插件目录 `rules/<owner>__<repo>.json`**——下次对同一仓库直接读缓存执行,**不再重复消耗 AI**;「重新分析(忽略缓存)」可强制重新思考
|
|
71
|
-
- **AI 生成 PR 内容**:填插件信息(owner/repo、分类、中英描述)→ AI 按规则模板生成标题 + 描述 + 条目文件内容,**生成后可自由修改,绝不直接提交**
|
|
72
|
-
- **执行 PR**:按规则步骤执行 `fork → 克隆 fork → 加 upstream → 拉取 → 建分支 → 写条目 → (重生成 README)→ 提交 → 推送 → 开 PR`(GitHub 走 `gh pr create`,Gitee 走 OpenAPI),逐步显示执行日志与 PR 链接;工作目录默认 = 默认工作区
|
|
73
|
-
|
|
74
|
-
### ⑥ 发布 npm 包(npm publish)
|
|
68
|
+
### ⑤ 发布 npm 包(npm publish)
|
|
75
69
|
- 五步向导(在目标目录执行):**① 确认源** `npm config get registry` → **② 查看认证配置** `npm config list`(自动脱敏 token/auth/password)→ **③ 验证身份** `npm whoami` → **④ 预览打包** `npm pack --dry-run`(先看会发布哪些文件,不真正打包发布)→ **⑤ 发布** `npm publish`
|
|
76
70
|
- 发布前必须勾选「我已核对以上内容,确认发布到 npm registry」才可点「发布」,防止误操作
|
|
77
71
|
|
|
@@ -213,7 +207,18 @@ dsh plugin --profile web add link:$(pwd)
|
|
|
213
207
|
|
|
214
208
|
## 版本历史
|
|
215
209
|
|
|
216
|
-
### v1.
|
|
210
|
+
### v1.14.0(当前)
|
|
211
|
+
按你的要求**移除了 ⑤「提交 PR」功能**:
|
|
212
|
+
|
|
213
|
+
- **前端**:`lib/client.js` 删除整个 `PrSection`(含 ⑤ 区块的渲染与面板导语中的「⑤提交 PR」文案),一并清理了对应的 i18n 键(`EN_DICT`)。
|
|
214
|
+
- **host**:`lib/index.js` 删除整套 PR 实现——`/pr/targets`、`/pr/rule`、`/pr/analyze`、`/pr/generate`、`/pr/execute` 五个路由,`prAnalyzeFlow` / `prGenerateFlow` / `prExecuteFlow`,PR 目标仓库本地存储(`source-code-mgmt-pr-targets.json`)、PR 规则缓存读写(`rules/` 下的 `readPrRule`/`writePrRule` 等)、`parseRepoUrl`、`fillPrTemplate`、安全校验(`safeEntryPath` / `safeRegenerateCommand`)、`PRESET_PR_RULES` 与启动时的规则预置写入。
|
|
215
|
+
- 保留 `llmComplete()` 这一通用 LLM 调用助手(与 PR 无关,属可复用能力,本轮暂未用到可直接忽略)。
|
|
216
|
+
- 版本号 1.13.0 → 1.14.0;`package.json` 描述同步去掉「AI-assisted Pull Requests」。克隆板块的「按地址克隆」「本地已有判定」与 ⑥ npm(默认空目录)等仍保持 v1.13.0 行为不变。
|
|
217
|
+
- 变更范围:`lib/client.js`(刷新即生效)、`lib/index.js`(需重启 dsh web)。
|
|
218
|
+
|
|
219
|
+
> 说明:`rules/` 目录下旧预置文件 `rules/awesome-dsh-plugin__awesome-dsh-plugin.json` 已不再被任何代码引用,可自行删除或保留(不影响功能)。
|
|
220
|
+
|
|
221
|
+
### v1.13.0(历史)
|
|
217
222
|
按第二轮反馈继续精化(④⑤⑥ 三处交互收敛):
|
|
218
223
|
|
|
219
224
|
- **④「本地已有」判定扩大到已登记的目录**:此前克隆列表只在「当前选中的克隆目录」下判定本地是否已有,用户若把克隆目录切到别处(如工作区 `workspace`),明明已存在的仓库仍标「可克隆」。现在新增 `localRepoExistsAnywhere()`:除了当前选中目录,还会遍历 ③ 代码管理登记过的**所有工作区 / 自定义目录**(`~/.dsh/storages/workspace.json` + `source-code-mgmt-dirs.json`)逐一判定——任一被登记的位置已有同名仓库即标「本地已有」;克隆成功后目标目录本就会记入自定义目录,因此「某位置已有该仓库」的知识会自动沉淀下来,下次列表直接命中。
|
package/lib/client.js
CHANGED
|
@@ -207,47 +207,9 @@ Object.assign(EN_DICT, {
|
|
|
207
207
|
"请先登录 GitHub CLI(gh)": "Log in to GitHub CLI (gh) first",
|
|
208
208
|
"需要先配置 Gitee 私人令牌": "Configure the Gitee personal token first",
|
|
209
209
|
"克隆一个账号下的远程仓库到本地": "Clone a remote repo from your account locally",
|
|
210
|
-
// ⑤ 提交 PR
|
|
211
|
-
"⑤ 提交 PR": "⑤ Pull Request",
|
|
212
|
-
"目标仓库地址": "Target repo URL",
|
|
213
|
-
"(如 https://github.com/owner/repo 或 owner/repo)": "(e.g. https://github.com/owner/repo or owner/repo)",
|
|
214
|
-
"记录到本地": "Save locally",
|
|
215
|
-
"已记录仓库": "Saved repos",
|
|
216
|
-
"分析 PR 规则": "Analyze PR rules",
|
|
217
|
-
"重新分析(忽略缓存)": "Re-analyze (ignore cache)",
|
|
218
|
-
"AI 正在思考该仓库的 PR 规则…": "AI is analyzing this repo's PR rules…",
|
|
219
|
-
"已使用缓存规则(插件目录 rules/,无需重新分析)": "Using cached rules (plugin rules/ dir, no re-analysis needed)",
|
|
220
|
-
"AI 已生成规则并缓存到插件目录 rules/": "AI generated rules and cached them in the plugin rules/ dir",
|
|
221
|
-
"规则步骤:": "Rule steps: ",
|
|
222
|
-
"上游分支(base)": "Upstream branch (base)",
|
|
223
|
-
"PR 标题": "PR title",
|
|
224
|
-
"PR 描述": "PR description",
|
|
225
|
-
"条目文件内容(entry,可留空)": "Entry file content (entry, optional)",
|
|
226
|
-
"插件信息(供 AI 生成 PR 描述)": "Plugin info (for AI to generate the PR description)",
|
|
227
|
-
"插件名 owner/repo": "Plugin name owner/repo",
|
|
228
|
-
"分类 category": "Category",
|
|
229
|
-
"描述 en": "Description (en)",
|
|
230
|
-
"描述 zh": "Description (zh)",
|
|
231
|
-
"生成 PR 内容(AI)": "Generate PR content (AI)",
|
|
232
|
-
"已生成,请确认 / 修改后再执行": "Generated — review / edit before running",
|
|
233
|
-
"执行 PR": "Run PR",
|
|
234
|
-
"PR 执行中…": "Running PR…",
|
|
235
|
-
"PR 已创建:": "PR created: ",
|
|
236
|
-
"PR 流程已完成": "PR flow completed",
|
|
237
|
-
"PR 执行失败": "PR flow failed",
|
|
238
|
-
"PR 执行失败:": "PR flow failed: ",
|
|
239
|
-
"工作目录(fork 克隆位置,默认=默认工作区)": "Working dir (where the fork is cloned; default = default workspace)",
|
|
240
|
-
"生成失败": "Generation failed",
|
|
241
|
-
"生成失败:": "Generation failed: ",
|
|
242
|
-
"分析失败": "Analysis failed",
|
|
243
|
-
"分析失败:": "Analysis failed: ",
|
|
244
|
-
"请填写目标仓库地址": "Enter the target repo URL first",
|
|
245
|
-
"请先填写仓库并分析规则": "Enter the repo and analyze its rules first",
|
|
246
|
-
"保存失败": "Save failed",
|
|
247
|
-
"目标仓库地址会记录到本地 ~/.dsh/storages(不写入插件目录)": "Target repo URLs are saved to local ~/.dsh/storages (not the plugin dir)",
|
|
248
210
|
"删除": "Remove",
|
|
249
|
-
//
|
|
250
|
-
"
|
|
211
|
+
// ⑤ 发布 npm 包
|
|
212
|
+
"⑤ 发布 npm 包": "⑤ Publish npm package",
|
|
251
213
|
"目标目录(含 package.json)": "Target dir (contains package.json)",
|
|
252
214
|
"① 确认源(registry)": "① Check registry",
|
|
253
215
|
"② 查看认证配置(config list)": "② Auth config (config list)",
|
|
@@ -267,7 +229,7 @@ Object.assign(EN_DICT, {
|
|
|
267
229
|
"发布失败:": "Publish failed: ",
|
|
268
230
|
"读取 npm 状态失败:": "Failed to read npm status: ",
|
|
269
231
|
"发布 npm 包五步向导:确认源 → 认证配置 → 身份 → 打包预览 → 发布": "Five-step npm publish guide: registry → auth config → identity → pack preview → publish",
|
|
270
|
-
"按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理,以及 ④克隆仓库 /
|
|
232
|
+
"按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理,以及 ④克隆仓库 / ⑤发布 npm 包。推送会自动忽略 >100MB 的文件(": "In order: ①Environment → ②SSH key & connection → ③Code management, plus ④Clone repos / ⑤Publish npm. Pushing auto-ignores >100MB files (",
|
|
271
233
|
" 限制)并说明原因。": " limit) with a reason.",
|
|
272
234
|
// ④ 目录选择(复用 ③ 的「选择目录」交互)
|
|
273
235
|
"克隆到目录(默认=用户主目录 home)": "Clone into (default = user home directory)",
|
|
@@ -276,18 +238,8 @@ Object.assign(EN_DICT, {
|
|
|
276
238
|
"已记住目录": "Directory remembered",
|
|
277
239
|
"记住目录失败:": "Failed to remember dir: ",
|
|
278
240
|
"目录不存在或不是文件夹:": "Directory does not exist or is not a folder: ",
|
|
279
|
-
// ⑤ 通用 PR 流程
|
|
280
|
-
"通用 PR 流程(未分析,可点击「分析 PR 规则」生成专属规则)": "Generic PR flow (not analyzed — click \"Analyze PR rules\" to generate a repo-specific one)",
|
|
281
|
-
"该仓库暂无本地规则": "No local rule for this repo yet",
|
|
282
|
-
"已加载本地规则(插件目录 rules/)": "Loaded local rule (plugin rules/ dir)",
|
|
283
|
-
"请先分析该仓库的 PR 规则再执行": "Analyze this repo's PR rules before running",
|
|
284
|
-
"已按模板生成(可编辑,未消耗 AI)": "Generated from the template (editable, no AI spent)",
|
|
285
|
-
"分析完成,规则已缓存到插件目录 rules/(下次直接使用,无需重新分析)": "Analyzed — rule cached to the plugin rules/ dir (used directly next time)",
|
|
286
|
-
"该仓库暂无本地规则,当前为通用 PR 流程": "No local rule for this repo yet — showing the generic PR flow",
|
|
287
|
-
"点击「分析 PR 规则」可让 AI 生成该仓库专属规则(并缓存到插件目录 rules/)": "Click \"Analyze PR rules\" to have AI generate a repo-specific rule (cached to the plugin rules/ dir)",
|
|
288
|
-
"已加载预置规则(插件目录 rules/)": "Preset rule loaded (plugin rules/ dir)",
|
|
289
241
|
"可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到常用目录并记住,下次打开无需重新选择。": "Type/paste an absolute path, or click \"Browse…\" to open the native folder picker. Confirming adds it to your favorite dirs so you won't re-pick it next time.",
|
|
290
|
-
//
|
|
242
|
+
// ⑤ npm 登录与目录
|
|
291
243
|
"未登录 npm,发布前需先登录": "Not logged in to npm — log in before publishing",
|
|
292
244
|
"打开终端执行 npm login": "Open a terminal to run npm login",
|
|
293
245
|
"复制登录命令": "Copy login command",
|
|
@@ -298,22 +250,7 @@ Object.assign(EN_DICT, {
|
|
|
298
250
|
"请手动在终端运行:": "Run this manually in a terminal: ",
|
|
299
251
|
"已复制:": "Copied: ",
|
|
300
252
|
"目录不存在:": "Directory does not exist: ",
|
|
301
|
-
"确保 fork": "Ensure fork",
|
|
302
|
-
"克隆 fork": "Clone fork",
|
|
303
|
-
"加上游": "Add upstream",
|
|
304
|
-
"拉取上游": "Fetch upstream",
|
|
305
|
-
"新建分支": "Create branch",
|
|
306
|
-
"写入条目": "Write entry",
|
|
307
253
|
"提交": "Commit",
|
|
308
|
-
"推送": "Push",
|
|
309
|
-
"创建 PR": "Create PR",
|
|
310
|
-
"该仓库已有规则,是否重新分析?": "A rule already exists for this repo — re-analyze?",
|
|
311
|
-
"分析 PR 规则(可选:手动填写规则或规则文件,留空则由 AI 自动查找)": "Analyze PR rules (optional: type the rules or a rule file; leave empty and AI will find them)",
|
|
312
|
-
"手动填写该仓库的 PR 规则(可选)": "Manually type this repo's PR rules (optional)",
|
|
313
|
-
"或提供规则文件(本地路径 / URL,如某仓库 README)": "Or provide a rules file (local path / URL, e.g. a repo README)",
|
|
314
|
-
"留空则由 AI 自动抓取该仓库文档(README / CONTRIBUTING / PR 模板)分析": "Leave empty and AI will auto-fetch this repo's docs (README / CONTRIBUTING / PR template)",
|
|
315
|
-
"开始分析": "Start analysis",
|
|
316
|
-
"已按你提供的规则分析并缓存到插件目录 rules/": "Analyzed with your supplied rules and cached to the plugin rules/ dir",
|
|
317
254
|
"取消": "Cancel",
|
|
318
255
|
// ④ 按地址克隆
|
|
319
256
|
"也可以按仓库地址克隆到任意位置": "You can also clone any repo by URL into any location",
|
|
@@ -322,12 +259,7 @@ Object.assign(EN_DICT, {
|
|
|
322
259
|
"请输入仓库地址": "Enter a repo URL",
|
|
323
260
|
"请选择克隆到目录": "Choose a destination directory first",
|
|
324
261
|
"无法从地址识别仓库名称": "Cannot derive a repo name from this URL",
|
|
325
|
-
// ⑤
|
|
326
|
-
"⑤ 提交 awesome-dsh-plugin 插件收录 PR": "⑤ Submit an awesome-dsh-plugin listing PR",
|
|
327
|
-
"目标仓库": "Target repo",
|
|
328
|
-
"已加载该仓库的收录规则(预置)": "Loaded this repo's listing rules (preset)",
|
|
329
|
-
"正在加载收录规则…": "Loading listing rules…",
|
|
330
|
-
// ⑥ 目录默认留空,需手动选择
|
|
262
|
+
// ⑤ 目录默认留空,需手动选择
|
|
331
263
|
"请先选择目标目录": "Choose a target directory first",
|
|
332
264
|
"请先选择目标目录(当前留空,不会自动使用默认目录)": "Choose a target directory first (left empty on purpose — it will NOT auto-use the default dir)",
|
|
333
265
|
});
|
|
@@ -1624,7 +1556,7 @@ function useSession() {
|
|
|
1624
1556
|
};
|
|
1625
1557
|
}
|
|
1626
1558
|
|
|
1627
|
-
/** Shared text input style for the new sections (
|
|
1559
|
+
/** Shared text input style for the new sections (④⑤). */
|
|
1628
1560
|
function inputStyle() {
|
|
1629
1561
|
return {
|
|
1630
1562
|
flex: 1, minWidth: 0, font: "inherit", fontSize: 12,
|
|
@@ -1980,135 +1912,9 @@ function useSession() {
|
|
|
1980
1912
|
);
|
|
1981
1913
|
}
|
|
1982
1914
|
|
|
1983
|
-
|
|
1984
|
-
// ⑤ 固定目标仓库:awesome-dsh-plugin 插件收录 PR(隐藏地址输入 / 记录 / 规则分析)。
|
|
1985
|
-
const PR_TARGET_URL = "https://github.com/awesome-dsh-plugin/awesome-dsh-plugin";
|
|
1986
|
-
function PrSection() {
|
|
1987
|
-
const [url, setUrl] = useState(PR_TARGET_URL);
|
|
1988
|
-
const [rule, setRule] = useState(null); // 预置规则
|
|
1989
|
-
const [ruleCached, setRuleCached] = useState(false);
|
|
1990
|
-
const [ruleSource, setRuleSource] = useState(null); // 'preset' | 'cached' | null
|
|
1991
|
-
const [workdir, setWorkdir] = useState("");
|
|
1992
|
-
const [pName, setPName] = useState("");
|
|
1993
|
-
const [pCategory, setPCategory] = useState("");
|
|
1994
|
-
const [pDescEn, setPDescEn] = useState("");
|
|
1995
|
-
const [pDescZh, setPDescZh] = useState("");
|
|
1996
|
-
const [title, setTitle] = useState("");
|
|
1997
|
-
const [body, setBody] = useState("");
|
|
1998
|
-
const [entry, setEntry] = useState("");
|
|
1999
|
-
const [genBusy, setGenBusy] = useState(false);
|
|
2000
|
-
const [execBusy, setExecBusy] = useState(false);
|
|
2001
|
-
const [logs, setLogs] = useState([]);
|
|
2002
|
-
const [prUrl, setPrUrl] = useState(null);
|
|
2003
|
-
const [msg, setMsg] = useState(null);
|
|
2004
|
-
const [err, setErr] = useState(null);
|
|
2005
|
-
|
|
2006
|
-
useEffect(() => { jget("/clone/default-dir").then((r) => { if (r && r.ok && r.dir) setWorkdir(r.dir); }).catch(() => {}); }, []);
|
|
2007
|
-
|
|
2008
|
-
// 加载 awesome-dsh-plugin 预置规则(只读,不触发 AI、不生成)。固定目标地址,仅挂载时查一次。
|
|
2009
|
-
const lookupRule = useCallback(async (u) => {
|
|
2010
|
-
const urlTxt = String(u || "").trim();
|
|
2011
|
-
try {
|
|
2012
|
-
const r = await jget("/pr/rule?url=" + encodeURIComponent(urlTxt));
|
|
2013
|
-
if (r && r.ok) {
|
|
2014
|
-
if (r.hasRule && r.rule) {
|
|
2015
|
-
setRule(r.rule);
|
|
2016
|
-
setRuleCached(true);
|
|
2017
|
-
setRuleSource(r.rule.preset ? "preset" : "cached");
|
|
2018
|
-
setErr(null);
|
|
2019
|
-
} else {
|
|
2020
|
-
setRule(null);
|
|
2021
|
-
setRuleCached(false);
|
|
2022
|
-
setRuleSource(null);
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
} catch (e) { /* 静默:保持现状 */ }
|
|
2026
|
-
}, []);
|
|
2027
|
-
useEffect(() => { void lookupRule(PR_TARGET_URL); }, [lookupRule]);
|
|
2028
|
-
|
|
2029
|
-
const generate = useCallback(async () => {
|
|
2030
|
-
const u = String(url || "").trim();
|
|
2031
|
-
if (!u || !rule) { setErr(t("请先填写仓库并分析规则")); return; }
|
|
2032
|
-
setGenBusy(true); setErr(null); setMsg(null);
|
|
2033
|
-
try {
|
|
2034
|
-
const r = await jpost("/pr/generate", {
|
|
2035
|
-
url: u, rule,
|
|
2036
|
-
pluginInfo: { name: pName, category: pCategory, descriptionEn: pDescEn, descriptionZh: pDescZh },
|
|
2037
|
-
});
|
|
2038
|
-
if (r && r.ok) {
|
|
2039
|
-
setTitle(r.title || ""); setBody(r.body || ""); setEntry(r.entry || "");
|
|
2040
|
-
setMsg(r.fromTemplate ? t("已按模板生成(可编辑,未消耗 AI)") : t("已生成,请确认 / 修改后再执行"));
|
|
2041
|
-
} else setErr((r && r.error) || t("生成失败"));
|
|
2042
|
-
} catch (e) { setErr(t("生成失败:") + e); }
|
|
2043
|
-
finally { setGenBusy(false); }
|
|
2044
|
-
}, [url, rule, pName, pCategory, pDescEn, pDescZh]);
|
|
2045
|
-
|
|
2046
|
-
const execute = useCallback(async () => {
|
|
2047
|
-
const u = String(url || "").trim();
|
|
2048
|
-
if (!u || !rule) { setErr(t("请先填写仓库并分析规则")); return; }
|
|
2049
|
-
setExecBusy(true); setErr(null); setMsg(null); setLogs([]); setPrUrl(null);
|
|
2050
|
-
try {
|
|
2051
|
-
const r = await jpost("/pr/execute", {
|
|
2052
|
-
url: u, workdir, rule, entryContent: entry, title, body,
|
|
2053
|
-
base: (rule && rule.baseBranch) || "main",
|
|
2054
|
-
});
|
|
2055
|
-
setLogs((r && r.logs) || []);
|
|
2056
|
-
if (r && r.ok) {
|
|
2057
|
-
if (r.prUrl) { setPrUrl(r.prUrl); setMsg(t("PR 已创建:") + r.prUrl); }
|
|
2058
|
-
else setMsg(t("PR 流程已完成"));
|
|
2059
|
-
} else setErr((r && r.error) || t("PR 执行失败"));
|
|
2060
|
-
} catch (e) { setErr(t("PR 执行失败:") + e); }
|
|
2061
|
-
finally { setExecBusy(false); }
|
|
2062
|
-
}, [url, rule, workdir, entry, title, body]);
|
|
2063
|
-
|
|
2064
|
-
return h(Box, { title: t("⑤ 提交 awesome-dsh-plugin 插件收录 PR"), defaultCollapsed: true },
|
|
2065
|
-
h("div", { style: { fontSize: 11, color: T.secondary, marginBottom: 8, lineHeight: 1.6 } },
|
|
2066
|
-
t("目标仓库") + ": https://github.com/awesome-dsh-plugin/awesome-dsh-plugin " +
|
|
2067
|
-
(rule ? t("已加载该仓库的收录规则(预置)") : t("正在加载收录规则…"))),
|
|
2068
|
-
h("div", { style: { border: "1px solid " + T.border, borderRadius: 8, padding: 8, marginBottom: 8 } },
|
|
2069
|
-
h("div", { style: { fontSize: 12, fontWeight: 600, color: T.label, marginBottom: 6 } }, t("插件信息(供 AI 生成 PR 描述)")),
|
|
2070
|
-
h(Field, { label: t("插件名 owner/repo") }, h("input", { type: "text", value: pName, placeholder: "Zhucy123/source-code-mgmt", onChange: (e) => setPName(e.target.value), style: inputStyle() })),
|
|
2071
|
-
h(Field, { label: t("分类 category") }, h("input", { type: "text", value: pCategory, placeholder: "git", onChange: (e) => setPCategory(e.target.value), style: inputStyle() })),
|
|
2072
|
-
h(Field, { label: t("描述 en") }, h("input", { type: "text", value: pDescEn, onChange: (e) => setPDescEn(e.target.value), style: inputStyle() })),
|
|
2073
|
-
h(Field, { label: t("描述 zh") }, h("input", { type: "text", value: pDescZh, onChange: (e) => setPDescZh(e.target.value), style: inputStyle() })),
|
|
2074
|
-
h("div", { style: { display: "flex", gap: 8, marginTop: 6 } },
|
|
2075
|
-
h(Btn, { label: genBusy ? t("生成中…") : t("生成 PR 内容(AI)"), onClick: generate, disabled: genBusy || !url || !rule, tone: "primary", noBg: true }),
|
|
2076
|
-
),
|
|
2077
|
-
),
|
|
2078
|
-
h("div", { style: { marginBottom: 6 } },
|
|
2079
|
-
h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 4 } }, t("PR 标题")),
|
|
2080
|
-
h("input", { type: "text", value: title, onChange: (e) => setTitle(e.target.value), style: inputStyle() }),
|
|
2081
|
-
),
|
|
2082
|
-
h("div", { style: { marginBottom: 6 } },
|
|
2083
|
-
h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 4 } }, t("PR 描述")),
|
|
2084
|
-
h("textarea", { rows: 3, value: body, onChange: (e) => setBody(e.target.value), style: { ...inputStyle(), minHeight: 60, resize: "vertical" } }),
|
|
2085
|
-
),
|
|
2086
|
-
h("div", { style: { marginBottom: 6 } },
|
|
2087
|
-
h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 4 } }, t("条目文件内容(entry,可留空)")),
|
|
2088
|
-
h("textarea", { rows: 4, value: entry, onChange: (e) => setEntry(e.target.value), style: { ...inputStyle(), minHeight: 70, resize: "vertical", fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 11 } }),
|
|
2089
|
-
),
|
|
2090
|
-
h("div", { style: { marginBottom: 6 } },
|
|
2091
|
-
h("div", { style: { color: T.secondary, fontSize: 12, marginBottom: 4 } }, t("工作目录(fork 克隆位置,默认=默认工作区)")),
|
|
2092
|
-
h("input", { type: "text", value: workdir, onChange: (e) => setWorkdir(e.target.value), style: inputStyle() }),
|
|
2093
|
-
),
|
|
2094
|
-
h("div", { style: { display: "flex", gap: 8, marginTop: 6, alignItems: "center", flexWrap: "wrap" } },
|
|
2095
|
-
h(Btn, { label: execBusy ? t("PR 执行中…") : t("执行 PR"), onClick: execute, disabled: execBusy || !url || !rule, tone: "success" }),
|
|
2096
|
-
prUrl ? h("a", { href: prUrl, target: "_blank", rel: "noreferrer", style: { color: T.brand, fontSize: 12 } }, prUrl) : null,
|
|
2097
|
-
),
|
|
2098
|
-
logs.length > 0 ? h("div", { style: { marginTop: 8, maxHeight: 200, overflow: "auto", background: T.layer1, borderRadius: 8, padding: 8 } },
|
|
2099
|
-
logs.map((lg, i) =>
|
|
2100
|
-
h("div", { key: i, style: { fontSize: 11, lineHeight: 1.6 } },
|
|
2101
|
-
h("span", { style: { color: lg.ok ? T.success : T.danger, fontWeight: 600, marginRight: 6 } }, (lg.ok ? "✅" : "❌") + " " + (lg.label || "")),
|
|
2102
|
-
lg.output ? h("span", { style: { color: T.secondary } }, lg.output) : null
|
|
2103
|
-
)
|
|
2104
|
-
)
|
|
2105
|
-
) : null,
|
|
2106
|
-
msg ? h("div", { style: { color: T.success, fontSize: 12, marginTop: 6 } }, "✅ " + msg) : null,
|
|
2107
|
-
err ? h("div", { style: { color: T.danger, fontSize: 12, marginTop: 6 } }, "❌ " + err) : null,
|
|
2108
|
-
);
|
|
2109
|
-
}
|
|
1915
|
+
|
|
2110
1916
|
|
|
2111
|
-
// ---------- section
|
|
1917
|
+
// ---------- section 5: 发布 npm 包 ----------
|
|
2112
1918
|
function NpmSection() {
|
|
2113
1919
|
const [dir, setDir] = useState("");
|
|
2114
1920
|
const [dirs, setDirs] = useState([]);
|
|
@@ -2132,7 +1938,7 @@ function useSession() {
|
|
|
2132
1938
|
jget("/workspaces").catch(() => null),
|
|
2133
1939
|
]);
|
|
2134
1940
|
const list = [];
|
|
2135
|
-
//
|
|
1941
|
+
// ⑤ 目标目录默认留空,由用户手动选择(不复选默认目录)。
|
|
2136
1942
|
if (def && def.dir) { if (!list.includes(def.dir)) list.push(def.dir); }
|
|
2137
1943
|
if (ws) {
|
|
2138
1944
|
if (Array.isArray(ws.workspaces)) for (const w of ws.workspaces) if (!list.includes(w)) list.push(w);
|
|
@@ -2220,7 +2026,7 @@ function useSession() {
|
|
|
2220
2026
|
{ key: "publish", label: t("⑤ 发布(publish)") },
|
|
2221
2027
|
];
|
|
2222
2028
|
|
|
2223
|
-
return h(Box, { title: t("
|
|
2029
|
+
return h(Box, { title: t("⑤ 发布 npm 包"), defaultCollapsed: true },
|
|
2224
2030
|
h("div", { style: { fontSize: 11, color: T.secondary, marginBottom: 8 } }, t("发布 npm 包五步向导:确认源 → 认证配置 → 身份 → 打包预览 → 发布")),
|
|
2225
2031
|
h("div", { style: { display: "flex", gap: 8, marginBottom: 8, alignItems: "center" } },
|
|
2226
2032
|
h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, t("目标目录(含 package.json)")),
|
|
@@ -2321,7 +2127,7 @@ function useSession() {
|
|
|
2321
2127
|
variant === "tab" ? null : h("button", { type: "button", style: closeBtnStyle(T), "aria-label": t("关闭"), onClick: onClose }, "✕")
|
|
2322
2128
|
),
|
|
2323
2129
|
h("p", { style: { margin: "6px 0 14px", color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
2324
|
-
t("按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理,以及 ④克隆仓库 /
|
|
2130
|
+
t("按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理,以及 ④克隆仓库 / ⑤发布 npm 包。推送会自动忽略 >100MB 的文件(") + (provider === "gitee" ? "Gitee" : "GitHub") + t(" 限制)并说明原因。")),
|
|
2325
2131
|
h(EnvSection, null),
|
|
2326
2132
|
h("div", { style: { height: 12 } }),
|
|
2327
2133
|
h(SshSection, { provider, setProvider }),
|
|
@@ -2330,8 +2136,6 @@ function useSession() {
|
|
|
2330
2136
|
h("div", { style: { height: 12 } }),
|
|
2331
2137
|
h(CloneSection, { provider, setProvider }),
|
|
2332
2138
|
h("div", { style: { height: 12 } }),
|
|
2333
|
-
h(PrSection, null),
|
|
2334
|
-
h("div", { style: { height: 12 } }),
|
|
2335
2139
|
h(NpmSection, null)
|
|
2336
2140
|
);
|
|
2337
2141
|
}
|
package/lib/index.js
CHANGED
|
@@ -30,7 +30,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
30
30
|
// ---------- i18n: host messages follow the client’s language (?lang= query param, per request) ----------
|
|
31
31
|
const HOST_EN = {"尚未配置 Gitee 私人令牌(请打开 ③ 代码管理输入令牌)":"No Gitee personal token configured (enter one in ③ Code Management)","Gitee API 请求失败(需要 curl)":"Gitee API request failed (curl required)","not a git repository(尚未 git init,可用「新建仓库并推送」初始化为 git 仓库并上传)":"not a git repository (run \"Create repo & push\" to init one and upload)","超过 GitHub 100MB 单文件限制(${}),已被 .gitignore 的 \"${}\" 排除":"Exceeds GitHub's 100MB file limit (${}) — already excluded by .gitignore entry \"${}\"","超过 GitHub 100MB 单文件限制(${}),${} 所在的一级目录「${}」整体忽略(该文件夹为一整体)":"Exceeds GitHub's 100MB file limit (${}) — the whole first-level folder \"${}\" of ${} is ignored (the folder is one unit)","超过 GitHub 100MB 单文件限制(${}),已忽略单个文件「${}」":"Exceeds GitHub's 100MB file limit (${}) — the file \"${}\" was ignored individually","尚未配置远程仓库 origin,请使用「新建仓库」创建远程仓库。":"No origin remote configured — use \"Create repo\" to create a remote repo.","not a git repository(尚未 git init,无法拉取)":"not a git repository (no git init yet — cannot pull)","尚未配置远程仓库 origin,无法拉取。":"No origin remote configured — cannot pull.","拉取存在冲突:本地有未合并改动或与远程冲突,请手动 git pull 处理合并。":"Pull conflicts: you have unmerged local changes or conflicts with the remote — resolve them manually with git pull.","git pull 失败":"git pull failed","not a git repository(尚未 git init)":"not a git repository (run git init first)","拉取并推送失败:合并存在冲突,请手动解决后重试。":"Pull-and-push failed: merge conflicts — resolve them manually and retry.","pull --rebase 失败":"pull --rebase failed","push 失败":"push failed","push --force 失败":"push --force failed","强制拉取存在冲突,请手动处理。":"Force-pull conflicts — handle them manually.","git pull --force 失败":"git pull --force failed","缺少仓库名称":"Repo name required","仓库名称包含非法字符(仅允许字母、数字、点、下划线、横线)":"Repo name contains invalid characters (only letters, digits, dots, underscores, dashes)","git add 失败:${}":"git add failed: ${}","需要先配置 Gitee 私人令牌":"Configure the Gitee personal token first","Gitee 创建仓库失败":"Failed to create the Gitee repo","缺少文件路径":"File path required","无法识别当前分支":"Cannot determine the current branch","尚未配置远程仓库 origin,无法对齐。":"No origin remote configured — cannot align.","git fetch 失败":"git fetch failed","git reset --hard 失败":"git reset --hard failed","git init 失败":"git init failed","git add 失败":"git add failed","git reset 失败":"git reset failed","提交信息不能为空":"Commit message cannot be empty","没有已暂存(staged)的改动可提交":"Nothing staged to commit","git commit 失败":"git commit failed","缺少分支名":"Branch name required","切换分支失败":"Failed to switch branch","git log 失败":"git log failed","缺少 commit hash":"Commit hash required","revert 失败(可能有冲突,请手动处理)":"revert failed (possible conflicts — handle manually)","cherry-pick 失败(可能有冲突,请手动处理)":"cherry-pick failed (possible conflicts — handle manually)","读取提交 diff 失败":"Failed to read the commit diff","未知工具:":"Unknown tool: ","未找到可用的包管理器(":"No usable package manager found (",")。请手动安装。":"). Please install manually.","安装成功":"Installed successfully","安装失败":"Installation failed","$dlg.Description = '选择本地目录';":"$dlg.Description = 'Choose a local folder';","Gitee 修改可见性失败":"Failed to change Gitee visibility","需要先登录 GitHub CLI(gh)":"Log in to the GitHub CLI (gh) first","修改可见性失败:${}":"Failed to change visibility: ${}","gh repo edit 失败":"gh repo edit failed","缺少目录路径":"Folder path required","未选择目录":"No folder selected","目录不存在或不是文件夹:":"Folder does not exist or is not a directory: ","令牌不能为空":"Token cannot be empty","Gitee 令牌无效,请检查(需有 projects 权限)":"Invalid Gitee token — check it (needs projects permission)"};
|
|
32
32
|
const requestLangStore = new AsyncLocalStorage();
|
|
33
|
-
// 新增板块(④克隆 / ⑤
|
|
33
|
+
// 新增板块(④克隆 / ⑤npm)的 host 英文消息,追加进 HOST_EN。
|
|
34
34
|
Object.assign(HOST_EN, {
|
|
35
35
|
"LLM 服务不可用(未加载 llm 服务)": "LLM service unavailable (llm service not loaded)",
|
|
36
36
|
"未找到默认模型配置(agent-default-model)": "No default model config found (agent-default-model)",
|
|
@@ -135,7 +135,7 @@ const SSH = resolveBin('ssh', 'DSH_SCM_SSH')
|
|
|
135
135
|
const SSH_KEYGEN = resolveBin('ssh-keygen', 'DSH_SCM_SSH_KEYGEN')
|
|
136
136
|
/** Resolved path to curl (used to call the Gitee OpenAPI). */
|
|
137
137
|
const CURL = resolveBin('curl', 'DSH_SCM_CURL')
|
|
138
|
-
/** Resolved path to npm (used by the
|
|
138
|
+
/** Resolved path to npm (used by the ⑤ publish-npm section). */
|
|
139
139
|
const NPM = resolveBin('npm', 'DSH_SCM_NPM')
|
|
140
140
|
|
|
141
141
|
/**
|
|
@@ -1859,7 +1859,7 @@ function installToolFlow(tool) {
|
|
|
1859
1859
|
}
|
|
1860
1860
|
|
|
1861
1861
|
// ---------------------------------------------------------------------------
|
|
1862
|
-
// ④ 克隆远程仓库 / ⑤
|
|
1862
|
+
// ④ 克隆远程仓库 / ⑤ 发布 npm 包 —— host 逻辑
|
|
1863
1863
|
// ---------------------------------------------------------------------------
|
|
1864
1864
|
|
|
1865
1865
|
// ---------- LLM 辅助:用 DSH 的 llm 服务 + 默认模型(当前对话/默认模型) ----------
|
|
@@ -1946,27 +1946,11 @@ function cloneHomeDir() {
|
|
|
1946
1946
|
return homedir()
|
|
1947
1947
|
}
|
|
1948
1948
|
|
|
1949
|
-
/**
|
|
1949
|
+
/** 默认克隆位置(⑤ 等板块的工作目录兜底):DSH 默认工作区。 */
|
|
1950
1950
|
function cloneDefaultDir() {
|
|
1951
1951
|
return resolveDefaultDir(process.cwd())
|
|
1952
1952
|
}
|
|
1953
1953
|
|
|
1954
|
-
/**
|
|
1955
|
-
* 从仓库 URL / SSH 地址 / owner/repo 解析平台、owner、repo。
|
|
1956
|
-
* @param {string} raw - https://github.com/owner/repo | git@github.com:owner/repo.git | owner/repo
|
|
1957
|
-
* @returns {{provider:'github'|'gitee', owner:string, repo:string}|{}}
|
|
1958
|
-
*/
|
|
1959
|
-
function parseRepoUrl(raw) {
|
|
1960
|
-
const url = String(raw || '').trim()
|
|
1961
|
-
if (!url) return {}
|
|
1962
|
-
const gh = /github\.com[/:]([^/\s?#]+)\/([^/\s?#]+)/i.exec(url)
|
|
1963
|
-
if (gh) return { provider: 'github', owner: gh[1], repo: gh[2].replace(/\.git$/i, '') }
|
|
1964
|
-
const gt = /gitee\.com[/:]([^/\s?#]+)\/([^/\s?#]+)/i.exec(url)
|
|
1965
|
-
if (gt) return { provider: 'gitee', owner: gt[1], repo: gt[2].replace(/\.git$/i, '') }
|
|
1966
|
-
const bare = /^([\w.-]+)\/([\w.-]+)$/.exec(url)
|
|
1967
|
-
if (bare) return { provider: 'github', owner: bare[1], repo: bare[2].replace(/\.git$/i, '') }
|
|
1968
|
-
return {}
|
|
1969
|
-
}
|
|
1970
1954
|
|
|
1971
1955
|
/** 某仓库名在 baseDir 下是否已有同名 git 仓库(含 baseDir 本身就是同名仓库的情形,
|
|
1972
1956
|
* 如工作区 C:\Users\xxx\workspace 即对应仓库 workspace)。 */
|
|
@@ -2087,610 +2071,8 @@ function cloneFlow(url, dest, name) {
|
|
|
2087
2071
|
return { ok: true, dir: full, name: repoName }
|
|
2088
2072
|
}
|
|
2089
2073
|
|
|
2090
|
-
// ---------- ⑤ 提交 PR(Pull Request) ----------
|
|
2091
|
-
|
|
2092
|
-
/** 用户填写的 PR 目标仓库列表(记录到 ~/.dsh/storages,不写入插件目录)。 */
|
|
2093
|
-
function prTargetsFile() {
|
|
2094
|
-
return join(homedir(), '.dsh', 'storages', 'source-code-mgmt-pr-targets.json')
|
|
2095
|
-
}
|
|
2096
|
-
function readPrTargets() {
|
|
2097
|
-
try {
|
|
2098
|
-
const f = prTargetsFile()
|
|
2099
|
-
if (!existsSync(f)) return []
|
|
2100
|
-
const data = JSON.parse(readFileSync(f, 'utf8'))
|
|
2101
|
-
return Array.isArray(data) ? data : []
|
|
2102
|
-
} catch { return [] }
|
|
2103
|
-
}
|
|
2104
|
-
function savePrTarget(url) {
|
|
2105
|
-
const { provider, owner, repo } = parseRepoUrl(url)
|
|
2106
|
-
if (!owner || !repo) return readPrTargets()
|
|
2107
|
-
const list = readPrTargets()
|
|
2108
|
-
const clean = list.filter((t) => String(t.url || '').trim().toLowerCase() !== String(url).trim().toLowerCase())
|
|
2109
|
-
clean.unshift({ url: String(url).trim(), provider, owner, repo, addedAt: new Date().toISOString() })
|
|
2110
|
-
try {
|
|
2111
|
-
const f = prTargetsFile()
|
|
2112
|
-
mkdirSync(dirname(f), { recursive: true })
|
|
2113
|
-
writeFileSync(f, JSON.stringify(clean.slice(0, 50), null, 2), 'utf8')
|
|
2114
|
-
} catch { /* ignore */ }
|
|
2115
|
-
return clean
|
|
2116
|
-
}
|
|
2117
|
-
function removePrTarget(url) {
|
|
2118
|
-
const list = readPrTargets().filter((t) => String(t.url || '').trim().toLowerCase() !== String(url || '').trim().toLowerCase())
|
|
2119
|
-
try {
|
|
2120
|
-
const f = prTargetsFile()
|
|
2121
|
-
mkdirSync(dirname(f), { recursive: true })
|
|
2122
|
-
writeFileSync(f, JSON.stringify(list, null, 2), 'utf8')
|
|
2123
|
-
} catch { /* ignore */ }
|
|
2124
|
-
return list
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
/** 插件根目录(用于存放 AI 思考出的 PR 规则缓存)。 */
|
|
2128
|
-
function pluginRoot() {
|
|
2129
|
-
return dirname(dirname(fileURLToPath(import.meta.url)))
|
|
2130
|
-
}
|
|
2131
|
-
function prRulesDir() {
|
|
2132
|
-
return join(pluginRoot(), 'rules')
|
|
2133
|
-
}
|
|
2134
|
-
function prRulePath(owner, repo) {
|
|
2135
|
-
return join(prRulesDir(), owner + '__' + repo + '.json')
|
|
2136
|
-
}
|
|
2137
|
-
function readPrRule(owner, repo) {
|
|
2138
|
-
try {
|
|
2139
|
-
const p = prRulePath(owner, repo)
|
|
2140
|
-
if (!existsSync(p)) return undefined
|
|
2141
|
-
const data = JSON.parse(readFileSync(p, 'utf8'))
|
|
2142
|
-
return data && typeof data === 'object' ? data : undefined
|
|
2143
|
-
} catch { return undefined }
|
|
2144
|
-
}
|
|
2145
|
-
function writePrRule(owner, repo, rule) {
|
|
2146
|
-
try {
|
|
2147
|
-
mkdirSync(prRulesDir(), { recursive: true })
|
|
2148
|
-
writeFileSync(prRulePath(owner, repo), JSON.stringify(rule, null, 2), 'utf8')
|
|
2149
|
-
return true
|
|
2150
|
-
} catch { return false }
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
/** 从 { 到 } 尽量提取 JSON(AI 可能包裹 markdown 代码块)。 */
|
|
2154
|
-
function parseJsonLoose(text) {
|
|
2155
|
-
const s = String(text || '').trim()
|
|
2156
|
-
try { return JSON.parse(s) } catch { /* continue */ }
|
|
2157
|
-
const start = s.indexOf('{')
|
|
2158
|
-
const end = s.lastIndexOf('}')
|
|
2159
|
-
if (start === -1 || end <= start) return null
|
|
2160
|
-
try { return JSON.parse(s.slice(start, end + 1)) } catch { return null }
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
/** 读取 GitHub 仓库内某文件的文本内容(gh api),失败返回 undefined。 */
|
|
2164
|
-
function ghFileText(owner, repo, path) {
|
|
2165
|
-
const r = run(GH, ['api', 'repos/' + owner + '/' + repo + '/contents/' + path, '--jq', '.content'], { timeout: 30_000 })
|
|
2166
|
-
if (!r.ok) return undefined
|
|
2167
|
-
try {
|
|
2168
|
-
const b64 = r.stdout.trim()
|
|
2169
|
-
if (!b64) return undefined
|
|
2170
|
-
return Buffer.from(b64, 'base64').toString('utf8')
|
|
2171
|
-
} catch { return undefined }
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
/** 抓取目标仓库的关键文档(README / CONTRIBUTING / PR 模板 / package.json scripts),供 AI 分析 PR 规则。 */
|
|
2175
|
-
async function gatherRepoDocs(provider, owner, repo) {
|
|
2176
|
-
const docs = {}
|
|
2177
|
-
const candidates = [
|
|
2178
|
-
'README.md', 'CONTRIBUTING.md', '.github/CONTRIBUTING.md',
|
|
2179
|
-
'.github/PULL_REQUEST_TEMPLATE.md', '.github/pull_request_template.md', 'package.json',
|
|
2180
|
-
]
|
|
2181
|
-
for (const c of candidates) {
|
|
2182
|
-
if (provider === 'gitee') {
|
|
2183
|
-
const api = giteeApi('GET', 'repos/' + owner + '/' + repo + '/contents/' + c)
|
|
2184
|
-
if (api.ok && api.data && typeof api.data.content === 'string') {
|
|
2185
|
-
try { docs[c] = Buffer.from(api.data.content, 'base64').toString('utf8') } catch {}
|
|
2186
|
-
}
|
|
2187
|
-
} else {
|
|
2188
|
-
const txt = ghFileText(owner, repo, c)
|
|
2189
|
-
if (txt !== undefined) docs[c] = txt
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
return docs
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
/** AI 思考 PR 规则的 system 提示词:要求输出结构化 JSON(步骤数组)。 */
|
|
2196
|
-
const PR_RULE_SYSTEM = [
|
|
2197
|
-
'你是一个开源仓库 PR 流程分析器。用户要往一个 GitHub/Gitee 仓库提交 Pull Request(提交内容通常是「把某个 DSH 插件收录进该仓库的插件列表 / 或按该仓库规则提交一份变更」)。',
|
|
2198
|
-
'请先分析该仓库的 PR 规则(基于 README / CONTRIBUTING / PR 模板 / package.json scripts),然后输出一个 JSON(不要任何多余文字、不要 markdown 代码块,直接输出 JSON 对象):',
|
|
2199
|
-
'{',
|
|
2200
|
-
' "baseBranch": "上游默认分支,如 main 或 master",',
|
|
2201
|
-
' "entryFilePath": "新增条目文件的相对路径(若适用;如 data/plugins/owner__repo.yml),否则 null",',
|
|
2202
|
-
' "entryTemplate": "条目文件的内容模板(含占位符,如 {NAME} {DESCRIPTION}),若适用;否则 null",',
|
|
2203
|
-
' "regenerateCommands": ["重生成 README 的命令数组(如 [\\"node\\", \\"scripts/generate-readme.mjs\\"]),没有则为 []"],',
|
|
2204
|
-
' "prTitle": "PR 标题建议,如 Add owner/repo",',
|
|
2205
|
-
' "prBodyHint": "PR 描述应包含的要点(中文一句话概括)",',
|
|
2206
|
-
' "steps": [',
|
|
2207
|
-
' { "type": "ensure-fork" },',
|
|
2208
|
-
' { "type": "clone-fork" },',
|
|
2209
|
-
' { "type": "add-upstream" },',
|
|
2210
|
-
' { "type": "fetch-upstream" },',
|
|
2211
|
-
' { "type": "branch", "name": "add-<插件名>" },',
|
|
2212
|
-
' { "type": "write-entry" },',
|
|
2213
|
-
' { "type": "regenerate-readme" },',
|
|
2214
|
-
' { "type": "commit", "message": "Add owner/repo" },',
|
|
2215
|
-
' { "type": "push" },',
|
|
2216
|
-
' { "type": "create-pr" }',
|
|
2217
|
-
' ]',
|
|
2218
|
-
'}',
|
|
2219
|
-
'要求:steps 按真实执行顺序排列,只包含上面列出的 type;不确定的字段给 null 或空数组,不要编造。',
|
|
2220
|
-
].join('\n')
|
|
2221
2074
|
|
|
2222
|
-
|
|
2223
|
-
* 用户已提供规则(手动文本或规则文件)时的 system 提示词:忠实整理为结构化 JSON,
|
|
2224
|
-
* 不额外抓取仓库文档、不编造——精简提示词以节省 token。
|
|
2225
|
-
*/
|
|
2226
|
-
const PR_RULE_SYSTEM_USER = [
|
|
2227
|
-
'你是一个开源仓库 PR 流程规则整理器。用户已直接提供该仓库的 PR 规则(手写文本或规则文件,如 README / CONTRIBUTING)。',
|
|
2228
|
-
'请把用户提供的规则忠实整理成下面的 JSON(不要任何多余文字、不要 markdown 代码块,直接输出 JSON 对象):',
|
|
2229
|
-
'{',
|
|
2230
|
-
' "baseBranch": "上游默认分支,如 main 或 master",',
|
|
2231
|
-
' "entryFilePath": "新增条目文件的相对路径(若适用;如 data/plugins/owner__repo.yml),否则 null",',
|
|
2232
|
-
' "entryTemplate": "条目文件的内容模板(含占位符,如 {NAME} {DESCRIPTION}),若适用;否则 null",',
|
|
2233
|
-
' "regenerateCommands": ["重生成 README 的命令数组(如 [\\"node\\", \\"scripts/generate-readme.mjs\\"]),没有则为 []"],',
|
|
2234
|
-
' "prTitle": "PR 标题建议,如 Add owner/repo",',
|
|
2235
|
-
' "prBodyHint": "PR 描述应包含的要点(中文一句话概括)",',
|
|
2236
|
-
' "steps": [',
|
|
2237
|
-
' { "type": "ensure-fork" },',
|
|
2238
|
-
' { "type": "clone-fork" },',
|
|
2239
|
-
' { "type": "add-upstream" },',
|
|
2240
|
-
' { "type": "fetch-upstream" },',
|
|
2241
|
-
' { "type": "branch", "name": "add-<插件名>" },',
|
|
2242
|
-
' { "type": "write-entry" },',
|
|
2243
|
-
' { "type": "regenerate-readme" },',
|
|
2244
|
-
' { "type": "commit", "message": "Add owner/repo" },',
|
|
2245
|
-
' { "type": "push" },',
|
|
2246
|
-
' { "type": "create-pr" }',
|
|
2247
|
-
' ]',
|
|
2248
|
-
'}',
|
|
2249
|
-
'要求:steps 按真实执行顺序排列,只包含上面列出的 type;只依据用户提供的规则整理,不要额外假设、不要编造用户没提到的字段。',
|
|
2250
|
-
].join('\n')
|
|
2251
|
-
|
|
2252
|
-
/**
|
|
2253
|
-
* 解析用户提供的规则来源:ruleText(手写文本)或 ruleFile(本地路径 / http(s) URL)。
|
|
2254
|
-
* @param {string} [ruleText] - 用户手动填写的 PR 规则文本。
|
|
2255
|
-
* @param {string} [ruleFile] - 规则文件路径或 URL(如某仓库 README)。
|
|
2256
|
-
* @returns {Promise<{text?:string, error?:string}>} 读取到的文本或错误。
|
|
2257
|
-
*/
|
|
2258
|
-
async function resolveRuleInput(ruleText, ruleFile) {
|
|
2259
|
-
const text = String(ruleText || '').trim()
|
|
2260
|
-
if (text) return { text }
|
|
2261
|
-
const file = String(ruleFile || '').trim()
|
|
2262
|
-
if (!file) return {}
|
|
2263
|
-
if (/^https?:\/\//i.test(file)) {
|
|
2264
|
-
// 远程 URL:优先 Node 全局 fetch,失败回退 curl。
|
|
2265
|
-
try {
|
|
2266
|
-
if (typeof fetch === 'function') {
|
|
2267
|
-
const ctrl = new AbortController()
|
|
2268
|
-
const timer = setTimeout(() => ctrl.abort(), 20000)
|
|
2269
|
-
try {
|
|
2270
|
-
const resp = await fetch(file, { redirect: 'follow', signal: ctrl.signal })
|
|
2271
|
-
if (resp.ok) {
|
|
2272
|
-
const body = await resp.text()
|
|
2273
|
-
if (body) return { text: body }
|
|
2274
|
-
}
|
|
2275
|
-
} finally { clearTimeout(timer) }
|
|
2276
|
-
}
|
|
2277
|
-
} catch { /* 继续尝试 curl */ }
|
|
2278
|
-
const r = run('curl', ['-sL', '--max-time', '20', file], { timeout: 30000 })
|
|
2279
|
-
if (r.ok && r.stdout) return { text: r.stdout }
|
|
2280
|
-
return { error: tr('规则文件获取失败:') + file }
|
|
2281
|
-
}
|
|
2282
|
-
if (existsSync(file)) {
|
|
2283
|
-
try { return { text: readFileSync(file, 'utf8') } }
|
|
2284
|
-
catch (e) { return { error: tr('规则文件读取失败:') + String((e && e.message) || e) } }
|
|
2285
|
-
}
|
|
2286
|
-
return { error: tr('找不到规则文件:') + file }
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
/**
|
|
2290
|
-
* 分析某仓库的 PR 规则:命中插件目录 rules/ 缓存则直接返回;否则抓取仓库文档(或用户提供的
|
|
2291
|
-
* 规则文本/文件)→ LLM 思考 → 保存缓存。
|
|
2292
|
-
* @param {*} ctx - cordis context。
|
|
2293
|
-
* @param {{url: string, force?: boolean, ruleText?: string, ruleFile?: string}} input -
|
|
2294
|
-
* 目标仓库 URL;force=true 忽略缓存重新分析;ruleText/ruleFile 为用户提供的规则来源(可选)。
|
|
2295
|
-
*/
|
|
2296
|
-
async function prAnalyzeFlow(ctx, { url, force, ruleText, ruleFile }) {
|
|
2297
|
-
const { provider, owner, repo } = parseRepoUrl(url)
|
|
2298
|
-
if (!owner || !repo) return { ok: false, error: tr('无法识别仓库地址:') + String(url || '') }
|
|
2299
|
-
// 用户提供了规则(文本或文件)→ 视为新的分析输入,忽略缓存直接分析。
|
|
2300
|
-
const hasUserInput = !!String(ruleText || '').trim() || !!String(ruleFile || '').trim()
|
|
2301
|
-
if (!force && !hasUserInput) {
|
|
2302
|
-
const cached = readPrRule(owner, repo)
|
|
2303
|
-
if (cached) return { ok: true, cached: true, provider, owner, repo, rule: cached }
|
|
2304
|
-
}
|
|
2305
|
-
let userRule = ''
|
|
2306
|
-
if (hasUserInput) {
|
|
2307
|
-
const r = await resolveRuleInput(ruleText, ruleFile)
|
|
2308
|
-
if (r.error) return { ok: false, error: r.error }
|
|
2309
|
-
userRule = String(r.text || '').trim()
|
|
2310
|
-
if (!userRule) return { ok: false, error: tr('未获取到规则内容(文件为空或无法读取)') }
|
|
2311
|
-
}
|
|
2312
|
-
let system, messages, docSources
|
|
2313
|
-
if (userRule) {
|
|
2314
|
-
system = PR_RULE_SYSTEM_USER
|
|
2315
|
-
messages = [{
|
|
2316
|
-
role: 'user',
|
|
2317
|
-
text: '目标仓库:' + provider + ' ' + owner + '/' + repo + '\n\n用户提供的该仓库 PR 规则(请忠实整理,不要编造):\n' + userRule.slice(0, 20000),
|
|
2318
|
-
}]
|
|
2319
|
-
docSources = []
|
|
2320
|
-
} else {
|
|
2321
|
-
const docs = await gatherRepoDocs(provider, owner, repo)
|
|
2322
|
-
system = PR_RULE_SYSTEM
|
|
2323
|
-
messages = [{
|
|
2324
|
-
role: 'user',
|
|
2325
|
-
text: '目标仓库:' + provider + ' ' + owner + '/' + repo + '\n\n仓库关键文档(尽力抓取,可能不全):\n'
|
|
2326
|
-
+ (Object.keys(docs).length ? JSON.stringify(docs, null, 2).slice(0, 12000) : '(无可用文档)'),
|
|
2327
|
-
}]
|
|
2328
|
-
docSources = Object.keys(docs)
|
|
2329
|
-
}
|
|
2330
|
-
const res = await llmComplete(ctx, { system, messages, maxTokens: 3000, temperature: 0.3 })
|
|
2331
|
-
if (!res.ok) return res
|
|
2332
|
-
const rule = parseJsonLoose(res.text)
|
|
2333
|
-
if (!rule || typeof rule !== 'object') return { ok: false, error: tr('AI 未输出有效规则') }
|
|
2334
|
-
if (!Array.isArray(rule.steps)) rule.steps = []
|
|
2335
|
-
rule.owner = owner; rule.repo = repo; rule.provider = provider
|
|
2336
|
-
rule.generatedAt = new Date().toISOString()
|
|
2337
|
-
rule.model = res.model
|
|
2338
|
-
rule.docSources = docSources
|
|
2339
|
-
if (userRule) rule.userProvided = true
|
|
2340
|
-
writePrRule(owner, repo, rule)
|
|
2341
|
-
return { ok: true, cached: false, provider, owner, repo, rule, model: res.model }
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
/**
|
|
2345
|
-
* 用 AI 生成 PR 内容(标题 + 描述 + 条目文件内容)。生成后由用户在 UI 里修改,不直接提交。
|
|
2346
|
-
* 当规则自带 entryTemplate 且插件信息齐全时,直接按模板本地填充(零 AI 消耗、可编辑);
|
|
2347
|
-
* 否则调用 LLM 生成。
|
|
2348
|
-
* @param {*} ctx - cordis context。
|
|
2349
|
-
* @param {{url: string, rule?: object, pluginInfo?: object}} input
|
|
2350
|
-
*/
|
|
2351
|
-
async function prGenerateFlow(ctx, { url, rule, pluginInfo }) {
|
|
2352
|
-
const { owner, repo } = parseRepoUrl(url)
|
|
2353
|
-
if (!owner || !repo) return { ok: false, error: tr('无法识别仓库地址:') + String(url || '') }
|
|
2354
|
-
const info = (pluginInfo && typeof pluginInfo === 'object') ? pluginInfo : {}
|
|
2355
|
-
const vars = { owner, repo, info }
|
|
2356
|
-
|
|
2357
|
-
// 模板直填路径:规则带 entryTemplate 时按 {VAR} 占位符本地生成(零 AI,仍可编辑)。
|
|
2358
|
-
if (rule && typeof rule.entryTemplate === 'string' && rule.entryTemplate !== '') {
|
|
2359
|
-
const entry = fillPrTemplate(rule.entryTemplate, vars)
|
|
2360
|
-
const title = fillPrTemplate(String(rule.prTitle || 'Add {NAME}'), vars) || ('Add ' + owner + '/' + repo)
|
|
2361
|
-
const body = fillPrTemplate(String(rule.prBodyHint || ''), vars)
|
|
2362
|
-
return {
|
|
2363
|
-
ok: true,
|
|
2364
|
-
title,
|
|
2365
|
-
body,
|
|
2366
|
-
entry,
|
|
2367
|
-
fromTemplate: true,
|
|
2368
|
-
provider: rule.provider || undefined,
|
|
2369
|
-
model: rule.model || 'template',
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
const system = [
|
|
2374
|
-
'你是 PR 内容生成助手。下面给出目标仓库的 PR 规则与用户要提交的插件信息,请生成三样内容并输出 JSON:',
|
|
2375
|
-
'{ "title": "PR 标题", "body": "PR 描述(markdown,说明改动内容、已满足的前置条件)", "entry": "条目文件内容(若规则有 entryFilePath/entryTemplate 则按模板生成;否则 null)" }',
|
|
2376
|
-
'直接输出 JSON,不要 markdown 代码块,不要多余文字。',
|
|
2377
|
-
].join('\n')
|
|
2378
|
-
const messages = [{
|
|
2379
|
-
role: 'user',
|
|
2380
|
-
text: '目标仓库规则:\n' + JSON.stringify(rule || {}, null, 2) + '\n\n用户要提交的插件信息:\n' + JSON.stringify(info, null, 2),
|
|
2381
|
-
}]
|
|
2382
|
-
const res = await llmComplete(ctx, { system, messages, maxTokens: 3000, temperature: 0.5 })
|
|
2383
|
-
if (!res.ok) return res
|
|
2384
|
-
const parsed = parseJsonLoose(res.text)
|
|
2385
|
-
if (!parsed || typeof parsed !== 'object') return { ok: false, error: tr('AI 输出无法解析') }
|
|
2386
|
-
return {
|
|
2387
|
-
ok: true,
|
|
2388
|
-
title: String(parsed.title || ''),
|
|
2389
|
-
body: String(parsed.body || ''),
|
|
2390
|
-
entry: parsed.entry == null ? null : String(parsed.entry),
|
|
2391
|
-
provider: res.provider,
|
|
2392
|
-
model: res.model,
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
/** 当前分支名(git rev-parse --abbrev-ref HEAD),失败回退 'main'。 */
|
|
2397
|
-
function currentBranch(dir) {
|
|
2398
|
-
const r = run(GIT, ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'])
|
|
2399
|
-
return r.ok && r.stdout.trim() && r.stdout.trim() !== 'HEAD' ? r.stdout.trim() : 'main'
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
/**
|
|
2403
|
-
* 校验 PR 规则里的条目文件路径:必须是仓库内相对路径,且解析后不得逃出 cloneDir
|
|
2404
|
-
* (防路径穿越 —— 规则来自 AI/缓存,可能被恶意仓库文档诱导出 "../.." 路径)。
|
|
2405
|
-
* @param {string} cloneDir - fork 克隆目录。
|
|
2406
|
-
* @param {string} p - 规则给出的 entryFilePath。
|
|
2407
|
-
* @returns {boolean} 安全则 true。
|
|
2408
|
-
*/
|
|
2409
|
-
function safeEntryPath(cloneDir, p) {
|
|
2410
|
-
if (typeof p !== 'string' || p === '') return false
|
|
2411
|
-
// 绝对路径(Windows 盘符 / POSIX 根 / 反斜杠)一律拒绝。
|
|
2412
|
-
if (/^([a-zA-Z]:[\\/]|\/|\\)/.test(p)) return false
|
|
2413
|
-
// 任何 .. 段(含 \..\、/../、..\ 前缀、../ 前缀)一律拒绝。
|
|
2414
|
-
if (/(^|[\\/])\.\.([\\/]|$)/.test(p)) return false
|
|
2415
|
-
const root = join(cloneDir, p)
|
|
2416
|
-
const base = cloneDir.replace(/[\\/]+$/, '')
|
|
2417
|
-
return root === base || root.startsWith(base + '\\') || root.startsWith(base + '/')
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
/**
|
|
2421
|
-
* 校验 PR 规则里的重生成命令:只允许常见构建/脚本命令 + 纯参数,拒绝绝对路径、
|
|
2422
|
-
* 环境变量展开、shell 元字符、重定向与明显危险的命令名。命令本身仍是数组传递
|
|
2423
|
-
* (execFileSync 不经过 shell),此校验是纵深防御,防止 AI 规则注入恶意命令。
|
|
2424
|
-
* @param {string[]} cmds - [bin, ...args]
|
|
2425
|
-
* @returns {boolean}
|
|
2426
|
-
*/
|
|
2427
|
-
const SAFE_REGENERATE_BINS = new Set([
|
|
2428
|
-
'node', 'npm', 'npx', 'pnpm', 'yarn', 'bun',
|
|
2429
|
-
'python', 'python3', 'py',
|
|
2430
|
-
'make', 'cmake',
|
|
2431
|
-
'sh', 'bash', 'zsh',
|
|
2432
|
-
'ruby', 'perl', 'php',
|
|
2433
|
-
'dotnet', 'go', 'cargo', 'rustc',
|
|
2434
|
-
'git', 'awk', 'sed', 'grep', 'cat',
|
|
2435
|
-
])
|
|
2436
|
-
// 参数不允许 shell 元字符 / 重定向 / 命令替换。命令名已由白名单约束(不经 shell,
|
|
2437
|
-
// 数组传递),此处只拦会被 shell 语义扩展、可能被后续误用的字符。
|
|
2438
|
-
const SHELL_META_PATTERN = /[|&;<>`$]/
|
|
2439
|
-
function safeRegenerateCommand(cmds) {
|
|
2440
|
-
// 兼容两种形态:单条 [bin, ...args] 或多条 [[bin,...],[bin,...]](顺序执行)。
|
|
2441
|
-
const list = (Array.isArray(cmds) && cmds.length > 0 && Array.isArray(cmds[0])) ? cmds : [cmds]
|
|
2442
|
-
for (const c of list) {
|
|
2443
|
-
if (!Array.isArray(c) || c.length === 0) return false
|
|
2444
|
-
const bin = String(c[0] || '').trim()
|
|
2445
|
-
if (bin === '') return false
|
|
2446
|
-
// 命令名必须是裸名称(不含路径分隔符),且在白名单内。
|
|
2447
|
-
if (/[\\/]/.test(bin)) return false
|
|
2448
|
-
if (!SAFE_REGENERATE_BINS.has(bin)) return false
|
|
2449
|
-
// 参数不允许 shell 元字符 / 重定向。
|
|
2450
|
-
for (let i = 1; i < c.length; i++) {
|
|
2451
|
-
const a = String(c[i] ?? '')
|
|
2452
|
-
if (SHELL_META_PATTERN.test(a)) return false
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
return true
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
/**
|
|
2459
|
-
* 用 {VAR} 占位符填充规则模板(OWNER/REPO/NAME/CATEGORY/DESC_EN/DESC_ZH)。
|
|
2460
|
-
* @param {string} tpl
|
|
2461
|
-
* @param {{owner:string,repo:string,info:object}} vars
|
|
2462
|
-
*/
|
|
2463
|
-
function fillPrTemplate(tpl, vars) {
|
|
2464
|
-
const map = {
|
|
2465
|
-
OWNER: vars.owner,
|
|
2466
|
-
REPO: vars.repo,
|
|
2467
|
-
NAME: String(vars.info && vars.info.name ? vars.info.name : vars.owner + '/' + vars.repo),
|
|
2468
|
-
CATEGORY: String(vars.info && vars.info.category ? vars.info.category : ''),
|
|
2469
|
-
DESC_EN: String(vars.info && vars.info.descriptionEn ? vars.info.descriptionEn : ''),
|
|
2470
|
-
DESC_ZH: String(vars.info && vars.info.descriptionZh ? vars.info.descriptionZh : ''),
|
|
2471
|
-
}
|
|
2472
|
-
return String(tpl ?? '').replace(/\{(OWNER|REPO|NAME|CATEGORY|DESC_EN|DESC_ZH)\}/g, (_, k) => map[k] ?? '')
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
/**
|
|
2476
|
-
* 预置规则:awesome-dsh-plugin 插件收录 PR(依据 提示词.md §2 流程)。
|
|
2477
|
-
* 写入插件目录 rules/awesome-dsh-plugin__awesome-dsh-plugin.json;已存在则不覆盖
|
|
2478
|
-
* (用户/AI 更新过的规则优先)。
|
|
2479
|
-
*/
|
|
2480
|
-
const PRESET_PR_RULES = [{
|
|
2481
|
-
owner: 'awesome-dsh-plugin',
|
|
2482
|
-
repo: 'awesome-dsh-plugin',
|
|
2483
|
-
rule: {
|
|
2484
|
-
provider: 'github',
|
|
2485
|
-
owner: 'awesome-dsh-plugin',
|
|
2486
|
-
repo: 'awesome-dsh-plugin',
|
|
2487
|
-
baseBranch: 'main',
|
|
2488
|
-
entryFilePath: 'data/plugins/{OWNER}__{REPO}.yml',
|
|
2489
|
-
entryTemplate: [
|
|
2490
|
-
'url: https://github.com/{OWNER}/{REPO}',
|
|
2491
|
-
'name: {NAME}',
|
|
2492
|
-
'category: {CATEGORY}',
|
|
2493
|
-
'description:',
|
|
2494
|
-
" en: '{DESC_EN}'",
|
|
2495
|
-
" zh: '{DESC_ZH}'",
|
|
2496
|
-
'',
|
|
2497
|
-
].join('\n'),
|
|
2498
|
-
regenerateCommands: [['npm', 'ci'], ['node', 'scripts/generate-readme.mjs']],
|
|
2499
|
-
prTitle: 'Add {NAME}',
|
|
2500
|
-
prBodyHint: '新增条目文件 data/plugins/{OWNER}__{REPO}.yml;已运行重生成命令更新 README;仓库已满足 dsh.bundle 声明、满 1 天、提交数 ≥10、dsh-plugin topic;描述已对照源码核实。',
|
|
2501
|
-
steps: [
|
|
2502
|
-
{ type: 'ensure-fork' },
|
|
2503
|
-
{ type: 'clone-fork' },
|
|
2504
|
-
{ type: 'add-upstream' },
|
|
2505
|
-
{ type: 'fetch-upstream' },
|
|
2506
|
-
{ type: 'branch', name: 'add-{REPO}' },
|
|
2507
|
-
{ type: 'write-entry' },
|
|
2508
|
-
{ type: 'regenerate-readme' },
|
|
2509
|
-
{ type: 'commit', message: 'Add {NAME}' },
|
|
2510
|
-
{ type: 'push' },
|
|
2511
|
-
{ type: 'create-pr' },
|
|
2512
|
-
],
|
|
2513
|
-
preset: true,
|
|
2514
|
-
generatedAt: '2026-09 (preset)',
|
|
2515
|
-
model: 'preset',
|
|
2516
|
-
},
|
|
2517
|
-
}]
|
|
2518
|
-
|
|
2519
|
-
/** 启动时把预置规则写入插件目录 rules/(仅当对应文件不存在)。 */
|
|
2520
|
-
function seedPresetPrRules() {
|
|
2521
|
-
for (const p of PRESET_PR_RULES) {
|
|
2522
|
-
if (readPrRule(p.owner, p.repo)) continue
|
|
2523
|
-
writePrRule(p.owner, p.repo, p.rule)
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
/**
|
|
2528
|
-
* 按规则执行 PR:fork → 克隆 fork → 加 upstream → 拉取 → 建分支 → 写条目 → 重生成 README →
|
|
2529
|
-
* 提交 → 推送 → 开 PR(GitHub 走 gh,Gitee 走 OpenAPI)。
|
|
2530
|
-
* @param {*} ctx - cordis context。
|
|
2531
|
-
* @param {{url: string, workdir?: string, rule?: object, entryContent?: string, title?: string, body?: string, base?: string}} input
|
|
2532
|
-
*/
|
|
2533
|
-
async function prExecuteFlow(ctx, { url, workdir, rule, entryContent, title, body, base }) {
|
|
2534
|
-
const { provider, owner, repo } = parseRepoUrl(url)
|
|
2535
|
-
if (!owner || !repo) return { ok: false, error: tr('无法识别仓库地址:') + String(url || '') }
|
|
2536
|
-
const steps = rule && Array.isArray(rule.steps) ? rule.steps : []
|
|
2537
|
-
if (steps.length === 0) return { ok: false, error: tr('没有可用的 PR 步骤(请先让 AI 分析规则)') }
|
|
2538
|
-
const work = String(workdir || '').trim() || cloneDefaultDir()
|
|
2539
|
-
const cloneDir = join(work, repo)
|
|
2540
|
-
const upstreamUrl = provider === 'gitee'
|
|
2541
|
-
? 'https://gitee.com/' + owner + '/' + repo + '.git'
|
|
2542
|
-
: 'https://github.com/' + owner + '/' + repo + '.git'
|
|
2543
|
-
const baseBranch = base || (rule && rule.baseBranch) || 'main'
|
|
2544
|
-
const logs = []
|
|
2545
|
-
const stepRun = (label, cmd, args, opts = {}) => {
|
|
2546
|
-
const r = run(cmd, args, { ...opts, timeout: opts.timeout ?? 300_000, cwd: opts.cwd || cloneDir, env: { ...process.env, GIT_SSH: WORKING_SSH, ...(opts.env || {}) } })
|
|
2547
|
-
logs.push({ label, ok: r.ok, output: ((r.stdout || '') + (r.stderr || '')).trim() })
|
|
2548
|
-
return r
|
|
2549
|
-
}
|
|
2550
|
-
for (const s of steps) {
|
|
2551
|
-
const type = s && typeof s === 'object' ? String(s.type || '') : String(s || '')
|
|
2552
|
-
switch (type) {
|
|
2553
|
-
case 'ensure-fork': {
|
|
2554
|
-
if (provider === 'gitee') {
|
|
2555
|
-
const o = giteeOwner()
|
|
2556
|
-
if (!o) return { ok: false, logs, error: tr('需要先配置 Gitee 私人令牌') }
|
|
2557
|
-
const api = giteeApi('POST', 'repos/' + owner + '/' + repo + '/forks')
|
|
2558
|
-
logs.push({ label: tr('确保 fork'), ok: api.ok || giteeApiFailed(api.data) === false, output: (api.error || tr('已 fork 或 fork 已存在')) })
|
|
2559
|
-
} else {
|
|
2560
|
-
const fr = run(GH, ['repo', 'fork', owner + '/' + repo, '--remote=false'], { timeout: 120_000, env: { GIT_SSH: WORKING_SSH } })
|
|
2561
|
-
const blob = ((fr.stdout || '') + (fr.stderr || '')).trim()
|
|
2562
|
-
// gh repo fork 在 fork 已存在时以非零退出并提示 "already exists",视为成功;
|
|
2563
|
-
// 其他失败(网络/权限)才记为失败。
|
|
2564
|
-
const already = /already exists/i.test(blob)
|
|
2565
|
-
logs.push({ label: tr('确保 fork'), ok: fr.ok || already, output: blob || tr('已 fork 或 fork 已存在') })
|
|
2566
|
-
}
|
|
2567
|
-
break
|
|
2568
|
-
}
|
|
2569
|
-
case 'clone-fork': {
|
|
2570
|
-
if (existsSync(cloneDir) && existsSync(join(cloneDir, '.git'))) {
|
|
2571
|
-
logs.push({ label: tr('克隆 fork'), ok: true, output: tr('已存在本地副本:') + cloneDir })
|
|
2572
|
-
} else {
|
|
2573
|
-
const me = provider === 'gitee' ? (giteeOwner() || owner) : (ghOwner() || owner)
|
|
2574
|
-
const forkUrl = provider === 'gitee'
|
|
2575
|
-
? ('git@gitee.com:' + me + '/' + repo + '.git')
|
|
2576
|
-
: ('git@github.com:' + me + '/' + repo + '.git')
|
|
2577
|
-
const r = run(GIT, ['clone', forkUrl, cloneDir], { timeout: 600_000, env: { GIT_SSH: WORKING_SSH } })
|
|
2578
|
-
logs.push({ label: tr('克隆 fork'), ok: r.ok, output: ((r.stdout || '') + (r.stderr || '')).trim() || tr('已克隆到 ') + cloneDir })
|
|
2579
|
-
if (!r.ok) return { ok: false, logs, error: tr('克隆 fork 失败') }
|
|
2580
|
-
}
|
|
2581
|
-
break
|
|
2582
|
-
}
|
|
2583
|
-
case 'add-upstream': {
|
|
2584
|
-
let r = stepRun(tr('添加上游'), GIT, ['remote', 'add', 'upstream', upstreamUrl])
|
|
2585
|
-
if (!r.ok) r = stepRun(tr('添加上游'), GIT, ['remote', 'set-url', 'upstream', upstreamUrl])
|
|
2586
|
-
logs[logs.length - 1].output = ((r.stdout || '') + (r.stderr || '')).trim() || upstreamUrl
|
|
2587
|
-
break
|
|
2588
|
-
}
|
|
2589
|
-
case 'fetch-upstream': {
|
|
2590
|
-
const r = stepRun(tr('拉取上游'), GIT, ['fetch', 'upstream'], { timeout: 120_000 })
|
|
2591
|
-
if (!r.ok) logs[logs.length - 1].output = ((r.stdout || '') + (r.stderr || '')).trim() || tr('拉取失败')
|
|
2592
|
-
break
|
|
2593
|
-
}
|
|
2594
|
-
case 'branch': {
|
|
2595
|
-
const raw = (s && s.name) || ('add-' + repo)
|
|
2596
|
-
const b = fillPrTemplate(String(raw), { owner, repo, info: {} })
|
|
2597
|
-
const r = stepRun(tr('新建分支'), GIT, ['checkout', '-B', b, 'upstream/' + baseBranch])
|
|
2598
|
-
if (!r.ok) {
|
|
2599
|
-
logs[logs.length - 1].output = ((r.stdout || '') + (r.stderr || '')).trim() || tr('创建分支失败')
|
|
2600
|
-
// 分支创建失败必须中止:后续 write-entry/commit/push 会在错误的当前分支上
|
|
2601
|
-
// 继续(例如推送到 fork 默认分支),可能造成非预期提交。
|
|
2602
|
-
return { ok: false, logs, error: tr('创建分支失败(上游分支可能不存在,请检查规则 baseBranch)') }
|
|
2603
|
-
}
|
|
2604
|
-
break
|
|
2605
|
-
}
|
|
2606
|
-
case 'write-entry': {
|
|
2607
|
-
const p = fillPrTemplate(String((s && s.path) || (rule && rule.entryFilePath) || '').trim(), { owner, repo, info: {} })
|
|
2608
|
-
if (!p) { logs.push({ label: tr('写入条目'), ok: false, output: tr('缺少条目文件路径') }); break }
|
|
2609
|
-
// 防路径穿越:条目路径必须安全落在 cloneDir 内(规则来自 AI/缓存,可能被诱导)。
|
|
2610
|
-
if (!safeEntryPath(cloneDir, p)) {
|
|
2611
|
-
logs.push({ label: tr('写入条目'), ok: false, output: tr('条目文件路径不安全(已拦截)') + ': ' + p })
|
|
2612
|
-
return { ok: false, logs, error: tr('条目文件路径不安全(已拦截)') }
|
|
2613
|
-
}
|
|
2614
|
-
try {
|
|
2615
|
-
mkdirSync(dirname(join(cloneDir, p)), { recursive: true })
|
|
2616
|
-
writeFileSync(join(cloneDir, p), entryContent || '', 'utf8')
|
|
2617
|
-
logs.push({ label: tr('写入条目'), ok: true, output: p })
|
|
2618
|
-
} catch (e) { logs.push({ label: tr('写入条目'), ok: false, output: String(e) }) }
|
|
2619
|
-
break
|
|
2620
|
-
}
|
|
2621
|
-
case 'regenerate-readme': {
|
|
2622
|
-
const raw = (Array.isArray(s && s.commands) ? s.commands : (rule && rule.regenerateCommands)) || []
|
|
2623
|
-
// 兼容两种形态:单条 [bin, ...args] 或多条 [[bin,...],[bin,...]](顺序执行)。
|
|
2624
|
-
const cmds = (Array.isArray(raw) && raw.length > 0 && Array.isArray(raw[0])) ? raw : [raw]
|
|
2625
|
-
if (cmds.length > 0) {
|
|
2626
|
-
// 防恶意规则:只允许白名单构建命令 + 安全参数(AI 规则可能注入任意命令)。
|
|
2627
|
-
if (!safeRegenerateCommand(cmds)) {
|
|
2628
|
-
logs.push({ label: tr('重生成 README'), ok: false, output: tr('重生成命令不安全(已拦截)') + ': ' + String(cmds[0] && cmds[0][0] || '') })
|
|
2629
|
-
return { ok: false, logs, error: tr('重生成命令不安全(已拦截)') }
|
|
2630
|
-
}
|
|
2631
|
-
let failed = false
|
|
2632
|
-
for (const c of cmds) {
|
|
2633
|
-
const r = stepRun(tr('重生成 README'), c[0], c.slice(1), { timeout: 180_000 })
|
|
2634
|
-
if (!r.ok) { logs[logs.length - 1].output = ((r.stdout || '') + (r.stderr || '')).trim() || tr('执行失败'); failed = true; break }
|
|
2635
|
-
}
|
|
2636
|
-
if (failed) return { ok: false, logs, error: tr('执行失败') }
|
|
2637
|
-
} else {
|
|
2638
|
-
logs.push({ label: tr('重生成 README'), ok: true, output: tr('无命令,跳过') })
|
|
2639
|
-
}
|
|
2640
|
-
break
|
|
2641
|
-
}
|
|
2642
|
-
case 'commit': {
|
|
2643
|
-
run(GIT, ['-C', cloneDir, 'add', '-A'])
|
|
2644
|
-
const ident = run(GIT, ['-C', cloneDir, 'config', 'user.email'])
|
|
2645
|
-
if (!ident.ok) {
|
|
2646
|
-
run(GIT, ['-C', cloneDir, 'config', 'user.name', 'DSH User'])
|
|
2647
|
-
run(GIT, ['-C', cloneDir, 'config', 'user.email', 'dsh@localhost'])
|
|
2648
|
-
}
|
|
2649
|
-
const msg = fillPrTemplate(String((s && s.message) || '').trim(), { owner, repo, info: {} }) || ('Add ' + owner + '/' + repo)
|
|
2650
|
-
const r = run(GIT, ['-C', cloneDir, 'commit', '-m', msg], { timeout: 60_000 })
|
|
2651
|
-
logs.push({ label: tr('提交'), ok: r.ok || /nothing to commit|no changes added/i.test(((r.stdout || '') + (r.stderr || ''))), output: ((r.stdout || '') + (r.stderr || '')).trim() || tr('已提交') })
|
|
2652
|
-
break
|
|
2653
|
-
}
|
|
2654
|
-
case 'push': {
|
|
2655
|
-
const r = stepRun(tr('推送'), GIT, ['push', '-u', 'origin', currentBranch(cloneDir)], { timeout: 180_000 })
|
|
2656
|
-
if (!r.ok) {
|
|
2657
|
-
logs[logs.length - 1].output = ((r.stdout || '') + (r.stderr || '')).trim() || tr('推送失败')
|
|
2658
|
-
return { ok: false, logs, error: tr('推送失败') }
|
|
2659
|
-
}
|
|
2660
|
-
break
|
|
2661
|
-
}
|
|
2662
|
-
case 'create-pr': {
|
|
2663
|
-
const titleTxt = String(title || '').trim()
|
|
2664
|
-
|| fillPrTemplate(String((s && s.title) || (rule && rule.prTitle) || ''), { owner, repo, info: {} })
|
|
2665
|
-
|| ('Add ' + owner + '/' + repo)
|
|
2666
|
-
const bodyTxt = String(body || '').trim()
|
|
2667
|
-
const branchName = currentBranch(cloneDir)
|
|
2668
|
-
if (provider === 'gitee') {
|
|
2669
|
-
const tok = readGiteeToken()
|
|
2670
|
-
const o = giteeOwner()
|
|
2671
|
-
if (!o || !tok) return { ok: false, logs, error: tr('需要先配置 Gitee 私人令牌') }
|
|
2672
|
-
const api = giteeApi('POST', 'repos/' + owner + '/' + repo + '/pulls', { title: titleTxt, head: o + ':' + branchName, base: baseBranch, body: bodyTxt || undefined }, tok)
|
|
2673
|
-
const urlM = api.ok && api.data && typeof api.data.html_url === 'string' ? api.data.html_url : undefined
|
|
2674
|
-
logs.push({ label: tr('创建 PR'), ok: api.ok, output: urlM || (api.error || (api.data && api.data.message) || tr('创建 PR 失败')) })
|
|
2675
|
-
return { ok: api.ok, logs, prUrl: urlM, error: api.ok ? undefined : (api.error || (api.data && api.data.message) || tr('创建 PR 失败')) }
|
|
2676
|
-
}
|
|
2677
|
-
const me = ghOwner() || owner
|
|
2678
|
-
const args = ['pr', 'create', '--repo', owner + '/' + repo, '--head', me + ':' + branchName, '--base', baseBranch, '--title', titleTxt]
|
|
2679
|
-
if (bodyTxt) args.push('--body', bodyTxt)
|
|
2680
|
-
const r = run(GH, args, { timeout: 180_000, cwd: cloneDir, env: { GIT_SSH: WORKING_SSH } })
|
|
2681
|
-
const blob = ((r.stdout || '') + (r.stderr || '')).trim()
|
|
2682
|
-
const urlM = /https:\/\/github\.com\/[^\s"]+/.exec(blob)
|
|
2683
|
-
logs.push({ label: tr('创建 PR'), ok: r.ok, output: blob || tr('已创建 PR') })
|
|
2684
|
-
return { ok: r.ok, logs, prUrl: urlM ? urlM[0] : undefined, error: r.ok ? undefined : (blob || tr('创建 PR 失败')) }
|
|
2685
|
-
}
|
|
2686
|
-
default:
|
|
2687
|
-
logs.push({ label: type || tr('未知步骤'), ok: false, output: tr('未知步骤类型') })
|
|
2688
|
-
}
|
|
2689
|
-
}
|
|
2690
|
-
return { ok: true, logs }
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
// ---------- ⑥ 发布 npm 包(npm publish) ----------
|
|
2075
|
+
// ---------- ⑤ 发布 npm 包(npm publish) ----------
|
|
2694
2076
|
|
|
2695
2077
|
/** npm 状态:是否安装、registry、whoami、目标目录是否存在 package.json。 */
|
|
2696
2078
|
function npmStatus(dir) {
|
|
@@ -3108,8 +2490,6 @@ function baseName(p) {
|
|
|
3108
2490
|
}
|
|
3109
2491
|
|
|
3110
2492
|
export function apply(ctx) {
|
|
3111
|
-
// 启动时把预置 PR 规则(awesome-dsh-plugin 等)写入插件目录 rules/(不存在才写)。
|
|
3112
|
-
seedPresetPrRules()
|
|
3113
2493
|
const fallbackDir = resolveDefaultDir(process.cwd())
|
|
3114
2494
|
// 每个请求按 ?lang= 参数跑在独立语言上下文里(AsyncLocalStorage),
|
|
3115
2495
|
// 所有 flow 函数内的 tr() 都能读到当前请求的语言,并发请求互不干扰。
|
|
@@ -3380,7 +2760,7 @@ export function apply(ctx) {
|
|
|
3380
2760
|
},
|
|
3381
2761
|
|
|
3382
2762
|
// ---- ④ 克隆远程仓库 ----
|
|
3383
|
-
//
|
|
2763
|
+
// 默认工作区(⑤ 等板块兜底)。
|
|
3384
2764
|
'/clone/default-dir': (req, res) => handle(req, res, async () => {
|
|
3385
2765
|
const dir = cloneDefaultDir()
|
|
3386
2766
|
return { ok: true, dir, name: baseName(dir) }
|
|
@@ -3407,64 +2787,7 @@ export function apply(ctx) {
|
|
|
3407
2787
|
})
|
|
3408
2788
|
},
|
|
3409
2789
|
|
|
3410
|
-
// ---- ⑤
|
|
3411
|
-
'/pr/targets': async (req, res) => {
|
|
3412
|
-
if (req.method === 'GET') {
|
|
3413
|
-
await handle(req, res, async () => ({ ok: true, targets: readPrTargets() }))
|
|
3414
|
-
} else if (req.method === 'POST') {
|
|
3415
|
-
await handle(req, res, async (req) => {
|
|
3416
|
-
const body = JSON.parse((await readBody(req)) || '{}')
|
|
3417
|
-
const url = String(body.url || '').trim()
|
|
3418
|
-
if (!url) return { ok: false, error: tr('缺少仓库地址') }
|
|
3419
|
-
if (body.remove) return { ok: true, targets: removePrTarget(url) }
|
|
3420
|
-
return { ok: true, targets: savePrTarget(url) }
|
|
3421
|
-
})
|
|
3422
|
-
} else {
|
|
3423
|
-
return methodNotAllowed(res, req.method)
|
|
3424
|
-
}
|
|
3425
|
-
},
|
|
3426
|
-
// 只读查询某仓库是否有缓存规则(命中插件目录 rules/,不触发 AI,不生成)。
|
|
3427
|
-
'/pr/rule': async (req, res) => {
|
|
3428
|
-
if (req.method !== 'GET') return methodNotAllowed(res, req.method)
|
|
3429
|
-
await handle(req, res, async (req) => {
|
|
3430
|
-
const q = new URL(req.url ?? '', 'http://localhost')
|
|
3431
|
-
const url = q.searchParams.get('url') || ''
|
|
3432
|
-
const { provider, owner, repo } = parseRepoUrl(url)
|
|
3433
|
-
if (!owner || !repo) return { ok: true, hasRule: false, url: String(url) }
|
|
3434
|
-
const cached = readPrRule(owner, repo)
|
|
3435
|
-
return {
|
|
3436
|
-
ok: true, hasRule: !!cached, url: String(url),
|
|
3437
|
-
provider, owner, repo,
|
|
3438
|
-
rule: cached || null,
|
|
3439
|
-
}
|
|
3440
|
-
})
|
|
3441
|
-
},
|
|
3442
|
-
'/pr/analyze': async (req, res) => {
|
|
3443
|
-
if (req.method !== 'POST') return methodNotAllowed(res, req.method)
|
|
3444
|
-
await handle(req, res, async (req) => {
|
|
3445
|
-
const body = JSON.parse((await readBody(req)) || '{}')
|
|
3446
|
-
return prAnalyzeFlow(ctx, { url: body.url, force: !!body.force, ruleText: body.ruleText, ruleFile: body.ruleFile })
|
|
3447
|
-
})
|
|
3448
|
-
},
|
|
3449
|
-
'/pr/generate': async (req, res) => {
|
|
3450
|
-
if (req.method !== 'POST') return methodNotAllowed(res, req.method)
|
|
3451
|
-
await handle(req, res, async (req) => {
|
|
3452
|
-
const body = JSON.parse((await readBody(req)) || '{}')
|
|
3453
|
-
return prGenerateFlow(ctx, { url: body.url, rule: body.rule, pluginInfo: body.pluginInfo })
|
|
3454
|
-
})
|
|
3455
|
-
},
|
|
3456
|
-
'/pr/execute': async (req, res) => {
|
|
3457
|
-
if (req.method !== 'POST') return methodNotAllowed(res, req.method)
|
|
3458
|
-
await handle(req, res, async (req) => {
|
|
3459
|
-
const body = JSON.parse((await readBody(req)) || '{}')
|
|
3460
|
-
return prExecuteFlow(ctx, {
|
|
3461
|
-
url: body.url, workdir: body.workdir, rule: body.rule,
|
|
3462
|
-
entryContent: body.entryContent, title: body.title, body: body.body, base: body.base,
|
|
3463
|
-
})
|
|
3464
|
-
})
|
|
3465
|
-
},
|
|
3466
|
-
|
|
3467
|
-
// ---- ⑥ 发布 npm 包 ----
|
|
2790
|
+
// ---- ⑤ 发布 npm 包 ----
|
|
3468
2791
|
'/npm/status': async (req, res) => {
|
|
3469
2792
|
if (req.method !== 'GET') return methodNotAllowed(res, req.method)
|
|
3470
2793
|
await handle(req, res, async (req) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "source-code-mgmt",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a button in the DSH session header's right-aligned utilities (beside \"Session log\") opening a right-side integrated panel that pushes the main content — env checks (git/gh), SSH ed25519 key setup, GitHub/Gitee dual-platform (SSH config 443 + Gitee OpenAPI), connectivity test, push / new repo, auto-ignore files over 100MB with per-file skipped reasons,
|
|
3
|
+
"version": "1.14.0",
|
|
4
|
+
"description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a button in the DSH session header's right-aligned utilities (beside \"Session log\") opening a right-side integrated panel that pushes the main content — env checks (git/gh), SSH ed25519 key setup, GitHub/Gitee dual-platform (SSH config 443 + Gitee OpenAPI), connectivity test, push / new repo, auto-ignore files over 100MB with per-file skipped reasons, a clone section (remote repos from your account with local-exists detection, plus clone any repo by URL into any location), and a five-step npm publish wizard. Cross-platform (Windows / Linux / macOS).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|