dsh-github-flow 0.1.7 → 0.1.8
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 +200 -67
- package/README.md +199 -65
- package/docs/architecture.md +55 -13
- package/docs/images/1.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3.png +0 -0
- package/lib/api/routes.d.ts +3 -1
- package/lib/api/routes.js +95 -4
- package/lib/client.js +851 -40
- package/lib/commands/gh.d.ts +2 -1
- package/lib/commands/gh.js +40 -3
- package/lib/executor.js +12 -2
- package/lib/main.d.ts +11 -0
- package/lib/main.js +41 -3
- package/lib/types.d.ts +45 -0
- package/lib/updater.d.ts +41 -0
- package/lib/updater.js +160 -0
- package/lib/upgrade-manager.d.ts +54 -0
- package/lib/upgrade-manager.js +299 -0
- package/lib/version.d.ts +13 -0
- package/lib/version.js +13 -0
- package/package.json +1 -1
package/lib/commands/gh.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { GhExecutor } from '../executor.js';
|
|
2
|
-
|
|
2
|
+
import type { UpdateChecker } from '../updater.js';
|
|
3
|
+
export declare function registerGhCommand(commandsService: any, executor: GhExecutor, updateChecker?: UpdateChecker): void;
|
package/lib/commands/gh.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { CURRENT_PLUGIN_VERSION, PLUGIN_PACKAGE_NAME, NPM_PACKAGE_URL } from '../version.js';
|
|
2
|
+
export function registerGhCommand(commandsService, executor, updateChecker) {
|
|
2
3
|
if (!commandsService || typeof commandsService.register !== 'function')
|
|
3
4
|
return;
|
|
4
5
|
commandsService.register({
|
|
@@ -11,7 +12,14 @@ export function registerGhCommand(commandsService, executor) {
|
|
|
11
12
|
// 「我刚 gh auth login 到底成没成」,给他 30 秒前的缓存等于答非所问。
|
|
12
13
|
const auth = await executor.checkAuth(undefined, true);
|
|
13
14
|
const repo = await executor.getRepoMetadata();
|
|
15
|
+
const updateInfo = updateChecker?.getQuickStatus();
|
|
14
16
|
let md = `### GitHub 集成状态\n\n`;
|
|
17
|
+
// 若检测到新版本,在最上方展示醒目的升级指引横幅
|
|
18
|
+
if (updateInfo?.hasUpdate) {
|
|
19
|
+
md += `> 🚀 **发现新版本 v${updateInfo.latestVersion}**(当前为 \`v${updateInfo.currentVersion}\`)\n`;
|
|
20
|
+
md += `> 输入 \`/gh upgrade\` 查看升级指引,或在终端执行:\`${updateInfo.upgradeCommand}\`\n\n`;
|
|
21
|
+
}
|
|
22
|
+
md += `- **插件版本**: \`v${updateInfo?.currentVersion || CURRENT_PLUGIN_VERSION}\`\n`;
|
|
15
23
|
if (auth.loggedIn) {
|
|
16
24
|
md += `- **登录账号**: \`${auth.user || '已登录'}\`\n`;
|
|
17
25
|
md += `- **授权作用域**: ${auth.scopes?.map((s) => `\`${s}\``).join(', ') || '标准权限'}\n`;
|
|
@@ -35,15 +43,44 @@ export function registerGhCommand(commandsService, executor) {
|
|
|
35
43
|
text: md,
|
|
36
44
|
};
|
|
37
45
|
}
|
|
46
|
+
if (trimmed === 'upgrade') {
|
|
47
|
+
const info = updateChecker ? await updateChecker.check(true) : null;
|
|
48
|
+
const curVer = info?.currentVersion || CURRENT_PLUGIN_VERSION;
|
|
49
|
+
const latestVer = info?.latestVersion || curVer;
|
|
50
|
+
const hasUpdate = info?.hasUpdate ?? false;
|
|
51
|
+
const isMac = process.platform === 'darwin';
|
|
52
|
+
const isWin = process.platform === 'win32';
|
|
53
|
+
const platformName = isMac ? 'macOS' : isWin ? 'Windows' : 'Linux';
|
|
54
|
+
let md = `### 🚀 ${PLUGIN_PACKAGE_NAME} 插件升级指南\n\n`;
|
|
55
|
+
md += `- **当前运行版本**: \`v${curVer}\`\n`;
|
|
56
|
+
md += `- **官方最新版本**: \`v${latestVer}\` ${hasUpdate ? '🔥 (有可用更新)' : '✅ (已是最新)'}\n`;
|
|
57
|
+
md += `- **操作系统环境**: \`${platformName} (${process.platform})\`\n`;
|
|
58
|
+
md += `- **npm 仓库地址**: [${NPM_PACKAGE_URL}](${NPM_PACKAGE_URL})\n\n`;
|
|
59
|
+
md += `#### 请根据你的 DSH 安装环境选择适用的升级方式:\n\n`;
|
|
60
|
+
md += `1. **DSH 官方标准工程升级(推荐)**:\n\`\`\`bash\npnpm dsh plugin add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
|
|
61
|
+
md += `2. **DSH 桌面版 Profile 环境升级**:\n\`\`\`bash\ndsh plugin --profile desktop add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n`;
|
|
62
|
+
if (isMac) {
|
|
63
|
+
md += `> 💡 *macOS 提示*:若在终端找不到 \`dsh\` 命令,可直接在桌面端目录更新:\n\`\`\`bash\ncd ~/.dsh/profiles/desktop && npm install ${PLUGIN_PACKAGE_NAME}@latest --save\n\`\`\`\n\n`;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
md += `\n`;
|
|
67
|
+
}
|
|
68
|
+
md += `3. **Node 全局环境升级**:\n\`\`\`bash\nnpm install -g ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
|
|
69
|
+
md += `> 💡 **升级后提示**:Node.js 运行时对已载入模块存在强缓存,升级后**请重启 DSH 服务以完全加载最新后端工具**。`;
|
|
70
|
+
return {
|
|
71
|
+
kind: 'success',
|
|
72
|
+
text: md,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
38
75
|
if (trimmed === 'help') {
|
|
39
76
|
return {
|
|
40
77
|
kind: 'success',
|
|
41
|
-
text: `### DSH GitHub Flow 帮助\n\n- \`/gh status\`: 检查当前 GitHub CLI
|
|
78
|
+
text: `### DSH GitHub Flow 帮助\n\n- \`/gh status\`: 检查当前 GitHub CLI 账号、仓库连接与插件版本\n- \`/gh upgrade\`: 检查新版本并显示全场景升级命令指引\n- \`/gh help\`: 显示此帮助信息\n\n可以在对话中直接让 Agent 操作 GitHub,如:\n- *"查看当前仓库有哪些开放的 PR"* \n- *"查看最近一次 CI 失败的原因"* \n- *"帮我给当前分支创建 PR"*`,
|
|
42
79
|
};
|
|
43
80
|
}
|
|
44
81
|
return {
|
|
45
82
|
kind: 'error',
|
|
46
|
-
text: `未知命令选项: /gh ${trimmed}。可用用法: /gh status, /gh help`,
|
|
83
|
+
text: `未知命令选项: /gh ${trimmed}。可用用法: /gh status, /gh upgrade, /gh help`,
|
|
47
84
|
};
|
|
48
85
|
},
|
|
49
86
|
});
|
package/lib/executor.js
CHANGED
|
@@ -269,7 +269,18 @@ export class GhExecutor {
|
|
|
269
269
|
async probeAuth(cwd) {
|
|
270
270
|
const res = await this.run(['auth', 'status'], { cwd, timeoutMs: 10_000, rawText: true });
|
|
271
271
|
const raw = (res.rawOutput || '') + '\n' + (res.error || '');
|
|
272
|
-
|
|
272
|
+
// 多重维度强健判定登录态 (容错 Windows Unicode 乱码、Keyring 告警与不同版本 CLI 输出差分)
|
|
273
|
+
const hasLoggedInFlag = /logged\s+in\s+to/i.test(raw) || raw.includes('✓') || raw.includes('√');
|
|
274
|
+
const hasActiveFlag = /active\s+account:\s*true/i.test(raw);
|
|
275
|
+
const hasTokenFlag = raw.includes('Token:') || raw.includes('Token scopes:');
|
|
276
|
+
const accountMatch = raw.match(/account\s+([A-Za-z0-9_-]+)/i);
|
|
277
|
+
const hasAccount = Boolean(accountMatch && accountMatch[1] && accountMatch[1].toLowerCase() !== 'none');
|
|
278
|
+
// 显式未登录特征排查
|
|
279
|
+
const isExplicitlyLoggedOut = /not\s+logged\s+in/i.test(raw) ||
|
|
280
|
+
raw.includes('gh auth login') ||
|
|
281
|
+
raw.includes('authentication required') ||
|
|
282
|
+
raw.includes('No accounts found');
|
|
283
|
+
const loggedIn = !isExplicitlyLoggedOut && (hasLoggedInFlag || hasActiveFlag || (hasTokenFlag && hasAccount));
|
|
273
284
|
// 只要已登录或有常规输出,则必然安装了 gh
|
|
274
285
|
const isNotInstalled = Boolean(res.error && (res.error.includes('系统未检测到 GitHub CLI') || res.error.includes('ENOENT')));
|
|
275
286
|
const installed = loggedIn || !isNotInstalled;
|
|
@@ -292,7 +303,6 @@ export class GhExecutor {
|
|
|
292
303
|
}
|
|
293
304
|
// 解析用户名
|
|
294
305
|
let user;
|
|
295
|
-
const accountMatch = raw.match(/account\s+([A-Za-z0-9_-]+)/i);
|
|
296
306
|
if (accountMatch) {
|
|
297
307
|
user = accountMatch[1];
|
|
298
308
|
}
|
package/lib/main.d.ts
CHANGED
|
@@ -2,6 +2,13 @@ import { GhExecutor } from './executor.js';
|
|
|
2
2
|
import './github-service.js';
|
|
3
3
|
import type { PluginConfig } from './types.js';
|
|
4
4
|
export declare const name = "dsh-github-flow";
|
|
5
|
+
/**
|
|
6
|
+
* 核心服务依赖声明
|
|
7
|
+
*
|
|
8
|
+
* 必须显式声明 webServer 与 workspaceRegistry:
|
|
9
|
+
* 在 Cordis 依赖隔离机制下,只有在 inject 显式声明的服务,当前插件作用域 ctx 才能持有
|
|
10
|
+
* ctx.webServer 与 ctx.workspaceRegistry 引用,进而注册 Web UI 所需的后端 API 路由。
|
|
11
|
+
*/
|
|
5
12
|
export declare const inject: readonly ['tools', 'commands', 'webServer', 'workspaceRegistry'];
|
|
6
13
|
export type Config = PluginConfig;
|
|
7
14
|
/**
|
|
@@ -19,4 +26,8 @@ export declare const Config: {
|
|
|
19
26
|
export declare function apply(ctx: any, config?: PluginConfig): void;
|
|
20
27
|
export type GitHubService = GhExecutor;
|
|
21
28
|
export * from './types.js';
|
|
29
|
+
export * from './version.js';
|
|
30
|
+
export * from './updater.js';
|
|
31
|
+
export * from './upgrade-manager.js';
|
|
32
|
+
export * from './executor.js';
|
|
22
33
|
export * from './github-service.js';
|
package/lib/main.js
CHANGED
|
@@ -6,10 +6,20 @@ import { createRepoTool } from './tools/repo.js';
|
|
|
6
6
|
import { createApiTool } from './tools/api.js';
|
|
7
7
|
import { registerGhCommand } from './commands/gh.js';
|
|
8
8
|
import { registerApiRoutes } from './api/routes.js';
|
|
9
|
+
import { UpdateChecker } from './updater.js';
|
|
10
|
+
import { UpgradeManager } from './upgrade-manager.js';
|
|
11
|
+
import { NPM_PACKAGE_URL } from './version.js';
|
|
9
12
|
// 引用即生效:加载本模块以挂上 declare module '@deepseek-ai/cordis' 的 ctx.github 类型声明,
|
|
10
13
|
// 并随下面 `export * from './github-service.js'` 一起传给消费方。
|
|
11
14
|
import './github-service.js';
|
|
12
15
|
export const name = 'dsh-github-flow';
|
|
16
|
+
/**
|
|
17
|
+
* 核心服务依赖声明
|
|
18
|
+
*
|
|
19
|
+
* 必须显式声明 webServer 与 workspaceRegistry:
|
|
20
|
+
* 在 Cordis 依赖隔离机制下,只有在 inject 显式声明的服务,当前插件作用域 ctx 才能持有
|
|
21
|
+
* ctx.webServer 与 ctx.workspaceRegistry 引用,进而注册 Web UI 所需的后端 API 路由。
|
|
22
|
+
*/
|
|
13
23
|
export const inject = ['tools', 'commands', 'webServer', 'workspaceRegistry'];
|
|
14
24
|
/**
|
|
15
25
|
* 符合 Standard Schema / Cordis Loader 契约的 Config 运行时校验规范
|
|
@@ -28,6 +38,9 @@ export const Config = {
|
|
|
28
38
|
maxOutputBytes: typeof cfg.maxOutputBytes === 'number' ? cfg.maxOutputBytes : undefined,
|
|
29
39
|
cacheTtlMs: typeof cfg.cacheTtlMs === 'number' ? cfg.cacheTtlMs : 15_000,
|
|
30
40
|
defaultListLimit: typeof cfg.defaultListLimit === 'number' ? cfg.defaultListLimit : 20,
|
|
41
|
+
checkUpdate: typeof cfg.checkUpdate === 'boolean' ? cfg.checkUpdate : true,
|
|
42
|
+
updateCheckIntervalMs: typeof cfg.updateCheckIntervalMs === 'number' ? cfg.updateCheckIntervalMs : 86_400_000,
|
|
43
|
+
registryUrl: typeof cfg.registryUrl === 'string' ? cfg.registryUrl : undefined,
|
|
31
44
|
}
|
|
32
45
|
};
|
|
33
46
|
}
|
|
@@ -35,6 +48,11 @@ export const Config = {
|
|
|
35
48
|
};
|
|
36
49
|
export function apply(ctx, config = {}) {
|
|
37
50
|
const executor = new GhExecutor(config);
|
|
51
|
+
const updateChecker = new UpdateChecker({
|
|
52
|
+
intervalMs: config.updateCheckIntervalMs,
|
|
53
|
+
customRegistry: config.registryUrl,
|
|
54
|
+
});
|
|
55
|
+
const upgradeManager = new UpgradeManager();
|
|
38
56
|
// 1. 兑现生态服务契约:将 GitHub 执行引擎作为服务暴露给其他 DSH 插件 (ctx.github)
|
|
39
57
|
if (ctx.provide && typeof ctx.provide === 'function') {
|
|
40
58
|
ctx.provide('github', executor);
|
|
@@ -42,9 +60,9 @@ export function apply(ctx, config = {}) {
|
|
|
42
60
|
else {
|
|
43
61
|
ctx.github = executor;
|
|
44
62
|
}
|
|
45
|
-
// 2. 注册人类 Slash 命令 (/gh status, /gh help)
|
|
63
|
+
// 2. 注册人类 Slash 命令 (/gh status, /gh upgrade, /gh help)
|
|
46
64
|
if (ctx.commands) {
|
|
47
|
-
registerGhCommand(ctx.commands, executor);
|
|
65
|
+
registerGhCommand(ctx.commands, executor, updateChecker);
|
|
48
66
|
}
|
|
49
67
|
// 3. 注册 Agent 模型工具集(5 大核心领域工具)
|
|
50
68
|
if (ctx.tools) {
|
|
@@ -58,9 +76,29 @@ export function apply(ctx, config = {}) {
|
|
|
58
76
|
if (ctx.webServer) {
|
|
59
77
|
// 传入 ctx 作用域对象,使 routes 能够通过 ctx.effect 绑定生命周期并在插件卸载/重载时自动注销;
|
|
60
78
|
// cacheTtlMs 传给路由层以尊重用户的缓存 TTL 配置。
|
|
61
|
-
registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000);
|
|
79
|
+
registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000, updateChecker, upgradeManager);
|
|
80
|
+
}
|
|
81
|
+
// 5. 启动后低优先级后台闲时静默探测:若有新版本,仅输出一次友好的控制台提示
|
|
82
|
+
if (config.checkUpdate !== false) {
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
updateChecker.check(false).then((info) => {
|
|
85
|
+
if (info.hasUpdate) {
|
|
86
|
+
console.log(`
|
|
87
|
+
┌────────────────────────────────────────────────────────────────────────┐
|
|
88
|
+
│ 🚀 dsh-github-flow 插件有新版本可用: ${info.currentVersion} -> ${info.latestVersion} │
|
|
89
|
+
│ 升级命令: ${info.upgradeCommand.padEnd(58, ' ')}│
|
|
90
|
+
│ 更新主页: ${NPM_PACKAGE_URL.padEnd(58, ' ')}│
|
|
91
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
}).catch(() => { });
|
|
95
|
+
}, 3_000);
|
|
62
96
|
}
|
|
63
97
|
}
|
|
64
98
|
export * from './types.js';
|
|
99
|
+
export * from './version.js';
|
|
100
|
+
export * from './updater.js';
|
|
101
|
+
export * from './upgrade-manager.js';
|
|
102
|
+
export * from './executor.js';
|
|
65
103
|
// 让 `ctx.github` 的类型声明随包一起被消费方加载(声明合并在 './github-service.js' 内)
|
|
66
104
|
export * from './github-service.js';
|
package/lib/types.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface PluginConfig {
|
|
|
14
14
|
cacheTtlMs?: number;
|
|
15
15
|
/** 查询列表默认限制条数,默认 20 */
|
|
16
16
|
defaultListLimit?: number;
|
|
17
|
+
/** 是否开启新版本自动检查提示,默认 true */
|
|
18
|
+
checkUpdate?: boolean;
|
|
19
|
+
/** 检查更新间隔毫秒数,默认 86,400,000 (24小时) */
|
|
20
|
+
updateCheckIntervalMs?: number;
|
|
21
|
+
/** 自定义 npm Registry 镜像地址,留空则自动选用 npmmirror 与 npmjs */
|
|
22
|
+
registryUrl?: string;
|
|
17
23
|
}
|
|
18
24
|
export interface GhExecutionOptions {
|
|
19
25
|
/** 命令执行的工作目录,优先使用当前 Agent 会话所在的 Workspace 物理路径 */
|
|
@@ -139,6 +145,8 @@ export interface GitHubOverviewData {
|
|
|
139
145
|
runs: WorkflowRunItem[];
|
|
140
146
|
lastUpdated: string;
|
|
141
147
|
error?: string;
|
|
148
|
+
/** 插件版本与升级信息(由内存缓存快速读取,不阻塞首屏业务) */
|
|
149
|
+
updateInfo?: UpdateInfo;
|
|
142
150
|
}
|
|
143
151
|
export interface UserRepoItem {
|
|
144
152
|
name: string;
|
|
@@ -199,4 +207,41 @@ export interface GlobalOverviewData {
|
|
|
199
207
|
error?: string;
|
|
200
208
|
/** 工作区矩阵读取失败的原因。区分「没有工作区」与「读不到工作区」,避免把故障显示成空状态 */
|
|
201
209
|
workspaceError?: string;
|
|
210
|
+
/** 插件版本与升级信息(由内存缓存快速读取,不阻塞首屏业务) */
|
|
211
|
+
updateInfo?: UpdateInfo;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 插件版本检查信息
|
|
215
|
+
*/
|
|
216
|
+
export interface UpdateInfo {
|
|
217
|
+
currentVersion: string;
|
|
218
|
+
latestVersion?: string;
|
|
219
|
+
hasUpdate: boolean;
|
|
220
|
+
upgradeCommand: string;
|
|
221
|
+
allCommands: {
|
|
222
|
+
dshCli: string;
|
|
223
|
+
profile: string;
|
|
224
|
+
pnpm: string;
|
|
225
|
+
npm: string;
|
|
226
|
+
};
|
|
227
|
+
releaseNotesUrl: string;
|
|
228
|
+
checkedAt: number;
|
|
229
|
+
}
|
|
230
|
+
export type UpgradeState = 'idle' | 'upgrading' | 'success' | 'failed';
|
|
231
|
+
export interface UpgradeStatusResult {
|
|
232
|
+
state: UpgradeState;
|
|
233
|
+
targetVersion?: string;
|
|
234
|
+
message?: string;
|
|
235
|
+
log?: string;
|
|
236
|
+
startedAt?: number;
|
|
237
|
+
completedAt?: number;
|
|
238
|
+
/** 识别到的宿主环境类型:desktop 为桌面版,web 为网页/CLI版 */
|
|
239
|
+
environment?: 'desktop' | 'web';
|
|
240
|
+
}
|
|
241
|
+
export interface UpgradeExecutionResult {
|
|
242
|
+
ok: boolean;
|
|
243
|
+
message: string;
|
|
244
|
+
needRestart: boolean;
|
|
245
|
+
log?: string;
|
|
246
|
+
environment?: 'desktop' | 'web';
|
|
202
247
|
}
|
package/lib/updater.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UpdateInfo } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 极简语义化版本大小比较函数 (latest > current 时返回 true)
|
|
4
|
+
*
|
|
5
|
+
* 为什么自研而不引入 semver npm 依赖:
|
|
6
|
+
* 避免为了简单的 x.y.z 比对而引入数十个传递子依赖,保持 DSH 插件轻量纯净与零运行期依赖负担。
|
|
7
|
+
* 约束条件:标准处理 x.y.z 形式,自动过滤前导 'v' 或 '^' 等非数字字符,忽略 pre-release 标签后续对比。
|
|
8
|
+
*/
|
|
9
|
+
export declare function isNewerVersion(current: string, latest: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* 插件版本检测引擎
|
|
12
|
+
*
|
|
13
|
+
* 架构约束与为什么这样设计:
|
|
14
|
+
* 1. 业务解耦:getQuickStatus 同步读取内存缓存,耗时 0ms,供主业务路由无感调用;
|
|
15
|
+
* 2. 闲时触发:check() 异步外网探测,首选淘宝镜像加速 (npmmirror.com),官方 npmjs 兜底;
|
|
16
|
+
* 3. 避免击穿:通过 inFlightPromise 合并并发调用,防止多处同时触发重复请求;
|
|
17
|
+
* 4. 彻底容灾:任何网络或解析异常 100% 内部吸收,返回 hasUpdate: false,绝不影响主流程。
|
|
18
|
+
*/
|
|
19
|
+
export declare class UpdateChecker {
|
|
20
|
+
private cachedInfo;
|
|
21
|
+
private inFlightPromise;
|
|
22
|
+
private readonly intervalMs;
|
|
23
|
+
private readonly customRegistry?;
|
|
24
|
+
constructor(options?: {
|
|
25
|
+
intervalMs?: number;
|
|
26
|
+
customRegistry?: string;
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* 同步获取当前已知的版本状态(0ms 纯内存读取,绝不发外网请求)
|
|
30
|
+
*/
|
|
31
|
+
getQuickStatus(): UpdateInfo;
|
|
32
|
+
/**
|
|
33
|
+
* 异步触发版本检测
|
|
34
|
+
* @param force 是否忽略 24h 缓存强制穿透探测
|
|
35
|
+
*/
|
|
36
|
+
check(force?: boolean): Promise<UpdateInfo>;
|
|
37
|
+
/**
|
|
38
|
+
* 内部双 Registry 竞速/降级探测
|
|
39
|
+
*/
|
|
40
|
+
private performCheck;
|
|
41
|
+
}
|
package/lib/updater.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { CURRENT_PLUGIN_VERSION, PLUGIN_PACKAGE_NAME, NPM_PACKAGE_URL, NPM_REGISTRY_MIRROR, NPM_REGISTRY_OFFICIAL, } from './version.js';
|
|
2
|
+
/**
|
|
3
|
+
* 极简语义化版本大小比较函数 (latest > current 时返回 true)
|
|
4
|
+
*
|
|
5
|
+
* 为什么自研而不引入 semver npm 依赖:
|
|
6
|
+
* 避免为了简单的 x.y.z 比对而引入数十个传递子依赖,保持 DSH 插件轻量纯净与零运行期依赖负担。
|
|
7
|
+
* 约束条件:标准处理 x.y.z 形式,自动过滤前导 'v' 或 '^' 等非数字字符,忽略 pre-release 标签后续对比。
|
|
8
|
+
*/
|
|
9
|
+
export function isNewerVersion(current, latest) {
|
|
10
|
+
if (!current || !latest)
|
|
11
|
+
return false;
|
|
12
|
+
const parseParts = (v) => {
|
|
13
|
+
return v
|
|
14
|
+
.replace(/^[^\d]*/, '')
|
|
15
|
+
.split('-')[0]
|
|
16
|
+
.split('.')
|
|
17
|
+
.map((part) => parseInt(part, 10) || 0);
|
|
18
|
+
};
|
|
19
|
+
const cParts = parseParts(current);
|
|
20
|
+
const lParts = parseParts(latest);
|
|
21
|
+
const len = Math.max(cParts.length, lParts.length, 3);
|
|
22
|
+
for (let i = 0; i < len; i++) {
|
|
23
|
+
const c = cParts[i] ?? 0;
|
|
24
|
+
const l = lParts[i] ?? 0;
|
|
25
|
+
if (l > c)
|
|
26
|
+
return true;
|
|
27
|
+
if (l < c)
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 插件版本检测引擎
|
|
34
|
+
*
|
|
35
|
+
* 架构约束与为什么这样设计:
|
|
36
|
+
* 1. 业务解耦:getQuickStatus 同步读取内存缓存,耗时 0ms,供主业务路由无感调用;
|
|
37
|
+
* 2. 闲时触发:check() 异步外网探测,首选淘宝镜像加速 (npmmirror.com),官方 npmjs 兜底;
|
|
38
|
+
* 3. 避免击穿:通过 inFlightPromise 合并并发调用,防止多处同时触发重复请求;
|
|
39
|
+
* 4. 彻底容灾:任何网络或解析异常 100% 内部吸收,返回 hasUpdate: false,绝不影响主流程。
|
|
40
|
+
*/
|
|
41
|
+
export class UpdateChecker {
|
|
42
|
+
cachedInfo = null;
|
|
43
|
+
inFlightPromise = null;
|
|
44
|
+
intervalMs;
|
|
45
|
+
customRegistry;
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
// 默认缓存 24 小时,杜绝高频击穿 npm
|
|
48
|
+
this.intervalMs = typeof options.intervalMs === 'number' && options.intervalMs > 0
|
|
49
|
+
? options.intervalMs
|
|
50
|
+
: 86_400_000;
|
|
51
|
+
this.customRegistry = options.customRegistry;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 同步获取当前已知的版本状态(0ms 纯内存读取,绝不发外网请求)
|
|
55
|
+
*/
|
|
56
|
+
getQuickStatus() {
|
|
57
|
+
if (this.cachedInfo) {
|
|
58
|
+
return this.cachedInfo;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
currentVersion: CURRENT_PLUGIN_VERSION,
|
|
62
|
+
hasUpdate: false,
|
|
63
|
+
upgradeCommand: `pnpm dsh plugin update ${PLUGIN_PACKAGE_NAME}`,
|
|
64
|
+
allCommands: {
|
|
65
|
+
dshCli: `pnpm dsh plugin update ${PLUGIN_PACKAGE_NAME}`,
|
|
66
|
+
profile: `dsh plugin --profile default add ${PLUGIN_PACKAGE_NAME}@latest`,
|
|
67
|
+
pnpm: `pnpm add ${PLUGIN_PACKAGE_NAME}@latest`,
|
|
68
|
+
npm: `npm install -g ${PLUGIN_PACKAGE_NAME}@latest`,
|
|
69
|
+
},
|
|
70
|
+
releaseNotesUrl: `${NPM_PACKAGE_URL}?activeTab=versions`,
|
|
71
|
+
checkedAt: 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 异步触发版本检测
|
|
76
|
+
* @param force 是否忽略 24h 缓存强制穿透探测
|
|
77
|
+
*/
|
|
78
|
+
async check(force = false) {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
// 1. 命中有效缓存直接返回
|
|
81
|
+
if (!force && this.cachedInfo && now - this.cachedInfo.checkedAt < this.intervalMs) {
|
|
82
|
+
return this.cachedInfo;
|
|
83
|
+
}
|
|
84
|
+
// 2. 复用正在途中的网络请求,避免并发重复打 npm
|
|
85
|
+
if (this.inFlightPromise) {
|
|
86
|
+
return this.inFlightPromise;
|
|
87
|
+
}
|
|
88
|
+
this.inFlightPromise = this.performCheck(now).finally(() => {
|
|
89
|
+
this.inFlightPromise = null;
|
|
90
|
+
});
|
|
91
|
+
return this.inFlightPromise;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 内部双 Registry 竞速/降级探测
|
|
95
|
+
*/
|
|
96
|
+
async performCheck(now) {
|
|
97
|
+
const fallback = this.getQuickStatus();
|
|
98
|
+
// 双通道策略:用户自定义源 > 国内淘宝镜像 (2.5s极速) > 官方 npmjs 源 (3.5s兜底)
|
|
99
|
+
const endpoints = [];
|
|
100
|
+
if (this.customRegistry) {
|
|
101
|
+
endpoints.push(`${this.customRegistry.replace(/\/$/, '')}/${PLUGIN_PACKAGE_NAME}/latest`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
endpoints.push(`${NPM_REGISTRY_MIRROR}/${PLUGIN_PACKAGE_NAME}/latest`);
|
|
105
|
+
endpoints.push(`${NPM_REGISTRY_OFFICIAL}/${PLUGIN_PACKAGE_NAME}/latest`);
|
|
106
|
+
}
|
|
107
|
+
let latestVersion = null;
|
|
108
|
+
for (const url of endpoints) {
|
|
109
|
+
try {
|
|
110
|
+
const isMirror = url.includes('npmmirror');
|
|
111
|
+
const timeoutMs = isMirror ? 2_500 : 3_500;
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
114
|
+
const res = await fetch(url, {
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
headers: {
|
|
117
|
+
'Accept': 'application/json',
|
|
118
|
+
'User-Agent': `dsh-plugin-updater/${CURRENT_PLUGIN_VERSION}`,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
if (res.ok) {
|
|
123
|
+
const json = (await res.json());
|
|
124
|
+
if (json && typeof json.version === 'string' && json.version.trim()) {
|
|
125
|
+
latestVersion = json.version.trim();
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// 单个源故障静默尝试下一个备用源,确保高可用
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 若双源均未拉取到版本,保留保底数据并记录时间戳,防止密集重试
|
|
135
|
+
if (!latestVersion) {
|
|
136
|
+
this.cachedInfo = {
|
|
137
|
+
...fallback,
|
|
138
|
+
checkedAt: now,
|
|
139
|
+
};
|
|
140
|
+
return this.cachedInfo;
|
|
141
|
+
}
|
|
142
|
+
const hasUpdate = isNewerVersion(CURRENT_PLUGIN_VERSION, latestVersion);
|
|
143
|
+
const targetPkg = `${PLUGIN_PACKAGE_NAME}@${latestVersion}`;
|
|
144
|
+
this.cachedInfo = {
|
|
145
|
+
currentVersion: CURRENT_PLUGIN_VERSION,
|
|
146
|
+
latestVersion,
|
|
147
|
+
hasUpdate,
|
|
148
|
+
upgradeCommand: `pnpm dsh plugin add ${targetPkg}`,
|
|
149
|
+
allCommands: {
|
|
150
|
+
dshCli: `pnpm dsh plugin add ${targetPkg}`,
|
|
151
|
+
profile: `dsh plugin --profile default add ${targetPkg}`,
|
|
152
|
+
pnpm: `pnpm add ${targetPkg}`,
|
|
153
|
+
npm: `npm install -g ${targetPkg}`,
|
|
154
|
+
},
|
|
155
|
+
releaseNotesUrl: `${NPM_PACKAGE_URL}?activeTab=versions`,
|
|
156
|
+
checkedAt: now,
|
|
157
|
+
};
|
|
158
|
+
return this.cachedInfo;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { UpgradeExecutionResult, UpgradeStatusResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 构建跨平台环境自愈增强的 ProcessEnv (重点解决 macOS GUI 应用丢失 PATH 的痛点)
|
|
4
|
+
*
|
|
5
|
+
* 为什么必须这样做:
|
|
6
|
+
* 1. 在 macOS 系统上,若 DSH 桌面客户端由 Finder / Dock / Spotlight (GUI) 启动,进程默认只继承
|
|
7
|
+
* 极简系统 PATH (/usr/bin:/bin:/usr/sbin:/sbin),不会自动读取用户 shell 配置文件 (~/.zshrc, ~/.bash_profile);
|
|
8
|
+
* 2. 用户通过 Homebrew (/opt/homebrew/bin 或 /usr/local/bin) 或 nvm 安装的 node、npm、pnpm 默认在 GUI 下不可见;
|
|
9
|
+
* 3. 动态将当前 node 运行时所在目录 (path.dirname(process.execPath)) 及 Mac 常用包管理器路径注入 PATH 首端,
|
|
10
|
+
* 彻底实现 macOS 与 Linux 环境下子进程对 npm / pnpm 命令的 100% 自愈发现。
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildCrossPlatformEnv(): NodeJS.ProcessEnv;
|
|
13
|
+
/**
|
|
14
|
+
* 自动升级执行引擎
|
|
15
|
+
*
|
|
16
|
+
* 核心设计考量与约束条件:
|
|
17
|
+
* 1. 互斥保护 (Mutex):同一时刻仅允许一个升级任务执行,防止用户连击或并发请求击垮包管理器 lockfile;
|
|
18
|
+
* 2. 环境自省 (Dev-Link Safe):检测是否在本地源码工作区调试,若是则阻断自动覆盖,保护源码不被破坏;
|
|
19
|
+
* 3. 跨平台兼容 (macOS / Windows / Linux):自动适配 cmd.exe 与 sh/zsh,自愈补全 macOS PATH 变量;
|
|
20
|
+
* 4. 稳态超时:设置 60s 进程上限,杜绝子进程因网络悬挂死锁宿主 WebServer;
|
|
21
|
+
* 5. 重启指引:Node.js 运行时内存不可变,升级成功后明确要求重启 DSH 彻底加载最新后端能力。
|
|
22
|
+
*/
|
|
23
|
+
export declare class UpgradeManager {
|
|
24
|
+
private isUpgrading;
|
|
25
|
+
private lastResult;
|
|
26
|
+
private logBuffer;
|
|
27
|
+
/**
|
|
28
|
+
* 探测当前插件是否是以本地 Git 源码软链接方式运行
|
|
29
|
+
*/
|
|
30
|
+
isDevLinked(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 智能识别当前 DSH 运行环境 (桌面版 vs Web/CLI 版)
|
|
33
|
+
*
|
|
34
|
+
* 依据事实源:
|
|
35
|
+
* 1. 桌面版启动时会固定注入 DSH_PROFILE=desktop 与 DSH_PROFILE_DIR (~/.dsh/profiles/desktop);
|
|
36
|
+
* 2. Web版/CLI版通常 DSH_PROFILE 为 default 或自定义工作区 profile;
|
|
37
|
+
* 3. 跨平台支持:通过 os.homedir() 安全解析 ~/.dsh 目录。
|
|
38
|
+
*/
|
|
39
|
+
detectEnvironment(): {
|
|
40
|
+
isDesktop: boolean;
|
|
41
|
+
profile: string;
|
|
42
|
+
profileDir?: string;
|
|
43
|
+
isDevLinked: boolean;
|
|
44
|
+
platform: NodeJS.Platform;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* 获取当前升级状态
|
|
48
|
+
*/
|
|
49
|
+
getStatus(): UpgradeStatusResult;
|
|
50
|
+
/**
|
|
51
|
+
* 执行一键升级核心流程
|
|
52
|
+
*/
|
|
53
|
+
executeUpgrade(targetVersion?: string): Promise<UpgradeExecutionResult>;
|
|
54
|
+
}
|