dsh-github-flow 0.6.0 → 0.7.1

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.
Files changed (95) hide show
  1. package/README.en.md +102 -15
  2. package/README.md +105 -15
  3. package/docs/UPGRADE-NOTES.md +12 -11
  4. package/docs/architecture.md +67 -14
  5. package/lib/api/route-helpers.d.ts +19 -0
  6. package/lib/api/route-helpers.js +54 -0
  7. package/lib/api/routes-actions.js +126 -2
  8. package/lib/api/routes-lifecycle.js +8 -1
  9. package/lib/api/routes-overview.d.ts +26 -0
  10. package/lib/api/routes-overview.js +276 -34
  11. package/lib/api/routes-proxy.js +8 -1
  12. package/lib/api/routes.d.ts +11 -1
  13. package/lib/api/routes.js +15 -2
  14. package/lib/cache.d.ts +0 -21
  15. package/lib/cache.js +0 -37
  16. package/lib/client.d.ts +2 -1
  17. package/lib/client.js +2108 -658
  18. package/lib/destructive.d.ts +59 -0
  19. package/lib/destructive.js +285 -0
  20. package/lib/executor/args-guard.d.ts +17 -0
  21. package/lib/executor/args-guard.js +23 -0
  22. package/lib/executor/failure-anchors.d.ts +52 -0
  23. package/lib/executor/failure-anchors.js +116 -0
  24. package/lib/executor/pr-review.d.ts +95 -0
  25. package/lib/executor/pr-review.js +175 -0
  26. package/lib/executor/run-log.d.ts +36 -0
  27. package/lib/executor/run-log.js +78 -0
  28. package/lib/executor.d.ts +204 -3
  29. package/lib/executor.js +670 -28
  30. package/lib/github-service.d.ts +1 -1
  31. package/lib/main.d.ts +92 -18
  32. package/lib/main.js +186 -95
  33. package/lib/renderers/common.d.ts +0 -3
  34. package/lib/renderers/common.js +8 -44
  35. package/lib/renderers/markdown.d.ts +1 -0
  36. package/lib/renderers/markdown.js +1 -0
  37. package/lib/renderers/pr.js +26 -1
  38. package/lib/renderers/task.d.ts +9 -0
  39. package/lib/renderers/task.js +120 -0
  40. package/lib/review-rules.d.ts +60 -0
  41. package/lib/review-rules.js +204 -0
  42. package/lib/shared/state-badges.d.ts +42 -0
  43. package/lib/shared/state-badges.js +102 -0
  44. package/lib/task-primitives.d.ts +109 -0
  45. package/lib/task-primitives.js +194 -0
  46. package/lib/tool-tiers.d.ts +57 -0
  47. package/lib/tool-tiers.js +168 -0
  48. package/lib/tools/agent-task.d.ts +4 -0
  49. package/lib/tools/agent-task.js +22 -9
  50. package/lib/tools/api.d.ts +4 -0
  51. package/lib/tools/api.js +48 -12
  52. package/lib/tools/body-input.d.ts +58 -0
  53. package/lib/tools/body-input.js +123 -0
  54. package/lib/tools/cache.js +42 -40
  55. package/lib/tools/codespace.js +4 -12
  56. package/lib/tools/discussion.d.ts +4 -0
  57. package/lib/tools/discussion.js +33 -17
  58. package/lib/tools/gist.d.ts +1 -39
  59. package/lib/tools/gist.js +4 -14
  60. package/lib/tools/help.js +65 -13
  61. package/lib/tools/issue.d.ts +4 -0
  62. package/lib/tools/issue.js +37 -38
  63. package/lib/tools/label.js +4 -15
  64. package/lib/tools/pr.d.ts +51 -0
  65. package/lib/tools/pr.js +204 -46
  66. package/lib/tools/project.js +4 -13
  67. package/lib/tools/release.d.ts +4 -0
  68. package/lib/tools/release.js +26 -26
  69. package/lib/tools/repo.js +4 -4
  70. package/lib/tools/ruleset.js +9 -6
  71. package/lib/tools/run.js +76 -24
  72. package/lib/tools/secret.js +11 -17
  73. package/lib/tools/skill.js +18 -22
  74. package/lib/tools/status.js +4 -4
  75. package/lib/tools/task.d.ts +223 -0
  76. package/lib/tools/task.js +464 -0
  77. package/lib/types.d.ts +183 -1
  78. package/lib/ui/commit-modal.js +108 -4
  79. package/lib/ui/diff-utils.d.ts +51 -0
  80. package/lib/ui/diff-utils.js +155 -0
  81. package/lib/ui/file-diff-modal.d.ts +33 -0
  82. package/lib/ui/file-diff-modal.js +250 -0
  83. package/lib/ui/help-modal.d.ts +1 -1
  84. package/lib/ui/help-modal.js +24 -12
  85. package/lib/ui/issue-modal.js +7 -3
  86. package/lib/ui/pr-modal.js +50 -11
  87. package/lib/ui/run-modal.d.ts +26 -0
  88. package/lib/ui/run-modal.js +486 -0
  89. package/lib/ui/styles.d.ts +1 -1
  90. package/lib/ui/styles.js +376 -44
  91. package/lib/ui/sync-settings-modal.js +1 -1
  92. package/lib/ui/workspace-panel.js +78 -23
  93. package/lib/version.d.ts +1 -1
  94. package/lib/version.js +1 -1
  95. package/package.json +9 -2
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 为什么需要这个文件:README 教生态插件这样复用本插件的能力——
5
5
  * export const inject = ['github']
6
- * export function apply(ctx) { await ctx.github.checkAuth() }
6
+ * export async function apply(ctx) { await ctx.github.checkAuth() }
7
7
  * 但在此之前项目里没有任何 `declare module` 声明合并,消费方拿到的 `ctx.github`
8
8
  * 类型是 unknown,照文档写会直接编译不过,也没有任何自动补全。
9
9
  * 官方每个服务包(如 @deepseek-ai/dsh-workspace、dsh-commands)都做这件事,这里补齐。
package/lib/main.d.ts CHANGED
@@ -1,29 +1,103 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import Schema from '@deepseek-ai/schemastery';
1
3
  import { GhExecutor } from './executor.js';
2
4
  import './github-service.js';
3
- import type { PluginConfig } from './types.js';
4
5
  export declare const name = "dsh-github-flow";
5
6
  /**
6
- * 核心服务依赖声明
7
+ * 服务依赖声明:只声明唯一的硬依赖 `tools`。
7
8
  *
8
- * 必须显式声明 webServer 与 workspaceRegistry:
9
- * 在 Cordis 依赖隔离机制下,只有在 inject 显式声明的服务,当前插件作用域 ctx 才能持有
10
- * ctx.webServer 与 ctx.workspaceRegistry 引用,进而注册 Web UI 所需的后端 API 路由。
9
+ * 为什么其余三个服务降级为可选(`ctx.inject` 等待):
10
+ * - `webServer` 由 `@deepseek-ai/dsh-web-app` 组合提供。`dsh-tui` 与纯 CLI profile 只装
11
+ * `dsh-base`,没有这个服务——写成硬依赖会让插件在这些 profile 里**永久 PENDING**,
12
+ * 用户一个工具都拿不到(README 明确支持 CLI 安装)。
13
+ * - `commands` 由 `dsh-commands` 提供,同样不保证所有组合都有。
14
+ *
15
+ * cordis 的语义是:`inject` 里的服务缺席 ⇒ 插件不激活。因此"声明了就必须真存在",
16
+ * 声明硬依赖却按可选写 `if (ctx.X)` 是最坏组合:既让可选服务变成了硬门槛,
17
+ * 又让降级分支永远走不到。
18
+ */
19
+ export declare const inject: readonly ['tools'];
20
+ /**
21
+ * 本插件向宿主提供的服务名。
22
+ * cordis loader 读取该元数据(registry.d.ts),与 `ctx.provide('github', …)` 必须同名。
11
23
  */
12
- export declare const inject: readonly ['tools', 'commands', 'webServer', 'workspaceRegistry'];
13
- export type Config = PluginConfig;
24
+ export declare const provide = "github";
14
25
  /**
15
- * 符合 Standard Schema / Cordis Loader 契约的 Config 运行时校验规范
26
+ * 插件配置 Schema(Schemastery)。
27
+ *
28
+ * 与旧的手写 `'~standard'.validate()` 相比,这里拿到的是**真校验**:类型不符直接抛
29
+ * `ValidationError`(旧实现对脏配置静默回落到默认值,配错了永远不报错)。
30
+ * 未知键仍原样透传,与旧行为一致。
31
+ *
32
+ * `.volatile()` 的三个字段是「运行期可热改、不触发插件 reload」的稳定引用:
33
+ * 读取点必须走 `.get()`,否则拿到的是引用对象本身(详见 `resolvePluginConfig`)。
34
+ *
35
+ * 为什么 `checkUpdate` 刻意**不** volatile:`UpdateChecker.enabled` 是构造期只读字段,
36
+ * 它同时决定轮询定时器与启动横幅是否建立。标成 volatile 只会得到「配置改了、行为没变」
37
+ * 的假热更新;让它保持普通字段,改动时插件整体重载,语义才是对的。
16
38
  */
17
- export declare const Config: {
18
- '~standard': {
19
- version: number;
20
- vendor: string;
21
- validate(value: unknown): {
22
- value: PluginConfig;
23
- };
24
- };
25
- };
26
- export declare function apply(ctx: any, config?: PluginConfig): void;
39
+ declare const ConfigSchema: Schema<Schemastery.ObjectS<NoInfer<{
40
+ ghPath: Schema<string, string, "defined">;
41
+ defaultTimeoutMs: Schema<number, number, "volatile-defined">;
42
+ maxOutputChars: Schema<number, number, "volatile-defined">;
43
+ maxOutputBytes: Schema<number, number, "plain">;
44
+ cacheTtlMs: Schema<number, number, "defined">;
45
+ defaultListLimit: Schema<number, number, "volatile-defined">;
46
+ checkUpdate: Schema<boolean, boolean, "defined">;
47
+ updateCheckIntervalMs: Schema<number, number, "defined">;
48
+ registryUrl: Schema<string, string, "plain">;
49
+ agentEnvInjection: Schema<boolean, boolean, "defined">;
50
+ proxyUrl: Schema<string, string, "plain">;
51
+ toolsets: Schema<string[], string[], "plain">;
52
+ read_only: Schema<boolean, boolean, "plain">;
53
+ }>>, Schemastery.ObjectT<NoInfer<{
54
+ ghPath: Schema<string, string, "defined">;
55
+ defaultTimeoutMs: Schema<number, number, "volatile-defined">;
56
+ maxOutputChars: Schema<number, number, "volatile-defined">;
57
+ maxOutputBytes: Schema<number, number, "plain">;
58
+ cacheTtlMs: Schema<number, number, "defined">;
59
+ defaultListLimit: Schema<number, number, "volatile-defined">;
60
+ checkUpdate: Schema<boolean, boolean, "defined">;
61
+ updateCheckIntervalMs: Schema<number, number, "defined">;
62
+ registryUrl: Schema<string, string, "plain">;
63
+ agentEnvInjection: Schema<boolean, boolean, "defined">;
64
+ proxyUrl: Schema<string, string, "plain">;
65
+ toolsets: Schema<string[], string[], "plain">;
66
+ read_only: Schema<boolean, boolean, "plain">;
67
+ }>>, "plain">;
68
+ /** 校验并补齐默认值后的运行期配置(volatile 字段为稳定引用,非普通值)。 */
69
+ export type Config = ReturnType<typeof ConfigSchema>;
70
+ /** Cordis loader 消费的配置 Schema。 */
71
+ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
72
+ ghPath: Schema<string, string, "defined">;
73
+ defaultTimeoutMs: Schema<number, number, "volatile-defined">;
74
+ maxOutputChars: Schema<number, number, "volatile-defined">;
75
+ maxOutputBytes: Schema<number, number, "plain">;
76
+ cacheTtlMs: Schema<number, number, "defined">;
77
+ defaultListLimit: Schema<number, number, "volatile-defined">;
78
+ checkUpdate: Schema<boolean, boolean, "defined">;
79
+ updateCheckIntervalMs: Schema<number, number, "defined">;
80
+ registryUrl: Schema<string, string, "plain">;
81
+ agentEnvInjection: Schema<boolean, boolean, "defined">;
82
+ proxyUrl: Schema<string, string, "plain">;
83
+ toolsets: Schema<string[], string[], "plain">;
84
+ read_only: Schema<boolean, boolean, "plain">;
85
+ }>>, Schemastery.ObjectT<NoInfer<{
86
+ ghPath: Schema<string, string, "defined">;
87
+ defaultTimeoutMs: Schema<number, number, "volatile-defined">;
88
+ maxOutputChars: Schema<number, number, "volatile-defined">;
89
+ maxOutputBytes: Schema<number, number, "plain">;
90
+ cacheTtlMs: Schema<number, number, "defined">;
91
+ defaultListLimit: Schema<number, number, "volatile-defined">;
92
+ checkUpdate: Schema<boolean, boolean, "defined">;
93
+ updateCheckIntervalMs: Schema<number, number, "defined">;
94
+ registryUrl: Schema<string, string, "plain">;
95
+ agentEnvInjection: Schema<boolean, boolean, "defined">;
96
+ proxyUrl: Schema<string, string, "plain">;
97
+ toolsets: Schema<string[], string[], "plain">;
98
+ read_only: Schema<boolean, boolean, "plain">;
99
+ }>>, "plain">;
100
+ export declare function apply(ctx: Context, config?: Config): void;
27
101
  export type GitHubService = GhExecutor;
28
102
  export * from './types.js';
29
103
  export * from './version.js';
package/lib/main.js CHANGED
@@ -1,3 +1,4 @@
1
+ import Schema from '@deepseek-ai/schemastery';
1
2
  import { GhExecutor } from './executor.js';
2
3
  import { createPrTool } from './tools/pr.js';
3
4
  import { createIssueTool } from './tools/issue.js';
@@ -17,6 +18,7 @@ import { createDiscussionTool } from './tools/discussion.js';
17
18
  import { createProjectTool } from './tools/project.js';
18
19
  import { createCodespaceTool } from './tools/codespace.js';
19
20
  import { createHelpTool } from './tools/help.js';
21
+ import { createTaskTool } from './tools/task.js';
20
22
  import { defaultTelemetryCollector } from './telemetry.js';
21
23
  import { registerGhCommand } from './commands/gh.js';
22
24
  import { registerApiRoutes } from './api/routes.js';
@@ -25,57 +27,121 @@ import { UpgradeManager } from './upgrade-manager.js';
25
27
  import { ProxyManager } from './proxy.js';
26
28
  import { NPM_PACKAGE_URL } from './version.js';
27
29
  import { loadUpdateSettings } from './update-settings.js';
30
+ import { applyTier } from './tool-tiers.js';
28
31
  // 引用即生效:加载本模块以挂上 declare module '@deepseek-ai/cordis' 的 ctx.github 类型声明,
29
32
  // 并随下面 `export * from './github-service.js'` 一起传给消费方。
30
33
  import './github-service.js';
31
34
  export const name = 'dsh-github-flow';
32
35
  /**
33
- * 核心服务依赖声明
36
+ * 服务依赖声明:只声明唯一的硬依赖 `tools`。
34
37
  *
35
- * 必须显式声明 webServer 与 workspaceRegistry:
36
- * 在 Cordis 依赖隔离机制下,只有在 inject 显式声明的服务,当前插件作用域 ctx 才能持有
37
- * ctx.webServer 与 ctx.workspaceRegistry 引用,进而注册 Web UI 所需的后端 API 路由。
38
+ * 为什么其余三个服务降级为可选(`ctx.inject` 等待):
39
+ * - `webServer` 由 `@deepseek-ai/dsh-web-app` 组合提供。`dsh-tui` 与纯 CLI profile 只装
40
+ * `dsh-base`,没有这个服务——写成硬依赖会让插件在这些 profile 里**永久 PENDING**,
41
+ * 用户一个工具都拿不到(README 明确支持 CLI 安装)。
42
+ * - `commands` 由 `dsh-commands` 提供,同样不保证所有组合都有。
43
+ *
44
+ * cordis 的语义是:`inject` 里的服务缺席 ⇒ 插件不激活。因此"声明了就必须真存在",
45
+ * 声明硬依赖却按可选写 `if (ctx.X)` 是最坏组合:既让可选服务变成了硬门槛,
46
+ * 又让降级分支永远走不到。
38
47
  */
39
- export const inject = ['tools', 'commands', 'webServer', 'workspaceRegistry'];
48
+ export const inject = ['tools'];
40
49
  /**
41
- * 符合 Standard Schema / Cordis Loader 契约的 Config 运行时校验规范
50
+ * 本插件向宿主提供的服务名。
51
+ * cordis loader 读取该元数据(registry.d.ts),与 `ctx.provide('github', …)` 必须同名。
42
52
  */
43
- export const Config = {
44
- '~standard': {
45
- version: 1,
46
- vendor: 'dsh',
47
- validate(value) {
48
- const cfg = (typeof value === 'object' && value !== null) ? value : {};
49
- return {
50
- value: {
51
- ghPath: typeof cfg.ghPath === 'string' ? cfg.ghPath : 'gh',
52
- defaultTimeoutMs: typeof cfg.defaultTimeoutMs === 'number' ? cfg.defaultTimeoutMs : 30_000,
53
- maxOutputChars: typeof cfg.maxOutputChars === 'number' ? cfg.maxOutputChars : 24_000,
54
- maxOutputBytes: typeof cfg.maxOutputBytes === 'number' ? cfg.maxOutputBytes : undefined,
55
- cacheTtlMs: typeof cfg.cacheTtlMs === 'number' ? cfg.cacheTtlMs : 15_000,
56
- defaultListLimit: typeof cfg.defaultListLimit === 'number' ? cfg.defaultListLimit : 20,
57
- checkUpdate: typeof cfg.checkUpdate === 'boolean' ? cfg.checkUpdate : true,
58
- agentEnvInjection: cfg.agentEnvInjection !== false,
59
- updateCheckIntervalMs: typeof cfg.updateCheckIntervalMs === 'number' ? cfg.updateCheckIntervalMs : 86_400_000,
60
- registryUrl: typeof cfg.registryUrl === 'string' ? cfg.registryUrl : undefined,
61
- proxyUrl: typeof cfg.proxyUrl === 'string' ? cfg.proxyUrl : undefined,
62
- }
63
- };
64
- }
65
- }
66
- };
67
- export function apply(ctx, config = {}) {
68
- const executor = new GhExecutor(config);
53
+ export const provide = 'github';
54
+ /**
55
+ * 插件配置 Schema(Schemastery)。
56
+ *
57
+ * 与旧的手写 `'~standard'.validate()` 相比,这里拿到的是**真校验**:类型不符直接抛
58
+ * `ValidationError`(旧实现对脏配置静默回落到默认值,配错了永远不报错)。
59
+ * 未知键仍原样透传,与旧行为一致。
60
+ *
61
+ * `.volatile()` 的三个字段是「运行期可热改、不触发插件 reload」的稳定引用:
62
+ * 读取点必须走 `.get()`,否则拿到的是引用对象本身(详见 `resolvePluginConfig`)。
63
+ *
64
+ * 为什么 `checkUpdate` 刻意**不** volatile:`UpdateChecker.enabled` 是构造期只读字段,
65
+ * 它同时决定轮询定时器与启动横幅是否建立。标成 volatile 只会得到「配置改了、行为没变」
66
+ * 的假热更新;让它保持普通字段,改动时插件整体重载,语义才是对的。
67
+ */
68
+ const ConfigSchema = Schema.object({
69
+ ghPath: Schema.string().default('gh'),
70
+ defaultTimeoutMs: Schema.number().default(30_000).volatile(),
71
+ maxOutputChars: Schema.number().default(24_000).volatile(),
72
+ // 不设默认值:缺省时由 maxOutputChars × 4 推导(executor 构造期),
73
+ // 显式给默认值会让「随 maxOutputChars 联动」这条语义失效。
74
+ maxOutputBytes: Schema.number(),
75
+ cacheTtlMs: Schema.number().default(15_000),
76
+ defaultListLimit: Schema.number().default(20).volatile(),
77
+ checkUpdate: Schema.boolean().default(true),
78
+ updateCheckIntervalMs: Schema.number().default(86_400_000),
79
+ registryUrl: Schema.string(),
80
+ agentEnvInjection: Schema.boolean().default(true),
81
+ proxyUrl: Schema.string(),
82
+ // 档位:缺省键直接缺席 → 下游按 full / 非只读处理,默认零破坏。
83
+ // 不设 default,是为了让「用户没配」与「用户显式配了 full」在诊断时仍可区分。
84
+ toolsets: Schema.array(Schema.string()),
85
+ read_only: Schema.boolean(),
86
+ });
87
+ /** Cordis loader 消费的配置 Schema。 */
88
+ export const Config = ConfigSchema;
89
+ /**
90
+ * 把 Schema 输出摊平为执行引擎消费的普通配置对象。
91
+ *
92
+ * 为什么必须显式摊平而不是 `{...config}`:volatile 字段是 `{ get(), [write]() }` 引用对象,
93
+ * 展开后仍是引用。`GhExecutor` 把它当数字用(`length > maxOutputChars`)不会报错,
94
+ * 只会让截断阈值变成 NaN 比较——静默失效,属于最难排查的一类回归。
95
+ * 逐字段列举而非遍历:新增配置项时这里会漏,但漏了 `tsc` 立刻报错,不会静默。
96
+ */
97
+ function resolvePluginConfig(config) {
98
+ return {
99
+ ghPath: config.ghPath,
100
+ defaultTimeoutMs: config.defaultTimeoutMs.get(),
101
+ maxOutputChars: config.maxOutputChars.get(),
102
+ maxOutputBytes: config.maxOutputBytes,
103
+ cacheTtlMs: config.cacheTtlMs,
104
+ defaultListLimit: config.defaultListLimit.get(),
105
+ checkUpdate: config.checkUpdate,
106
+ updateCheckIntervalMs: config.updateCheckIntervalMs,
107
+ // 可选字符串字段在 Schema 里没有 default,缺省时键直接缺席,显式 null 也会被放行;
108
+ // 统一归一成 undefined,下游 `if (x)` 语义才不会被 null 误伤。
109
+ registryUrl: config.registryUrl ?? undefined,
110
+ agentEnvInjection: config.agentEnvInjection,
111
+ proxyUrl: config.proxyUrl ?? undefined,
112
+ toolsets: config.toolsets,
113
+ read_only: config.read_only,
114
+ };
115
+ }
116
+ /**
117
+ * 监听宿主事件。
118
+ *
119
+ * 为什么要这层收窄:`ctx.on` 的类型参数是 `keyof Events`,而本插件依赖的两个事件
120
+ * 声明都在本仓之外——`tools/result` 来自 `@deepseek-ai/dsh-tools`,
121
+ * `loader/volatile-update` 来自 `@deepseek-ai/cordis-plugin-loader`。
122
+ * 本仓不能直接依赖前者:它的 peer 闭包引用了未发布的 `@deepseek-ai/dsh-type-meta`
123
+ * (npm 404),`pnpm install` 直接失败。
124
+ *
125
+ * 为什么不用 `declare module '@deepseek-ai/cordis'` 自行合并 `Events`:消费方若同时加载
126
+ * dsh-tools,两份方法签名会合成重载,而本仓这份 `any` 签名会把对方的精确类型盖掉。
127
+ * 只在本文件内收窄,不污染全局契约。
128
+ */
129
+ function onHostEvent(ctx, name, listener) {
130
+ ctx.on(name, listener);
131
+ }
132
+ export function apply(ctx, config = Config({})) {
133
+ const plain = resolvePluginConfig(config);
134
+ const executor = new GhExecutor(plain);
69
135
  // 代理管理器:优先读取用户在驾驶舱保存的 proxy.json,回落 cordis.patch.yml 的
70
136
  // proxyUrl 预置值;两者皆无时保持直连(gh 自然继承宿主系统环境变量)。
71
137
  // 装配后 executor/upgradeManager/updater 三条外联链路全部经它路由。
72
- const proxyManager = new ProxyManager(config.proxyUrl);
138
+ const proxyManager = new ProxyManager(plain.proxyUrl);
73
139
  executor.attachProxyManager(proxyManager);
74
140
  const updateChecker = new UpdateChecker({
75
- intervalMs: config.updateCheckIntervalMs,
76
- customRegistry: config.registryUrl,
141
+ intervalMs: plain.updateCheckIntervalMs,
142
+ customRegistry: plain.registryUrl,
77
143
  // checkUpdate: false 熔断全部探测入口(路由/命令/启动横幅共用此实例)
78
- enabled: config.checkUpdate !== false,
144
+ enabled: plain.checkUpdate,
79
145
  });
80
146
  // 更新源设置(UI 持久化 store 优先于 Cordis 静态配置):
81
147
  // 启动时读取并应用到 checker;UI 保存时经路由直接调 applySettings 热切换。
@@ -87,65 +153,96 @@ export function apply(ctx, config = {}) {
87
153
  // 注入查询函数而非静态值:用户切源后,下一次一键升级的 registry 实时跟随
88
154
  upgradeManager.attachUpdateModeQuery(() => updateChecker.getSourceMode());
89
155
  // 1. 兑现生态服务契约:将 GitHub 执行引擎作为服务暴露给其他 DSH 插件 (ctx.github)
90
- if (ctx.provide && typeof ctx.provide === 'function') {
91
- ctx.provide('github', executor);
92
- }
93
- else {
94
- ctx.github = executor;
95
- }
156
+ // 服务注册由当前 fiber 拥有:返回的 disposer 或 fiber 卸载时自动注销(cordis reflect.d.ts:29-43),
157
+ // 因此不需要 `ctx.github = executor` 这类裸赋值兜底——它绕过 inject 语义且永不清理。
158
+ ctx.provide('github', executor);
96
159
  // 2. 注册 Cordis tools/result 事件切面(无侵入全量工具审计与降级率打点)
97
- if (ctx.on && typeof ctx.on === 'function') {
98
- ctx.on('tools/result', (exec, result) => {
99
- try {
100
- const toolName = exec?.name || exec?.tool?.name || '';
101
- if (typeof toolName === 'string' && toolName.startsWith('github_')) {
102
- const args = exec?.arguments || exec?.args || {};
103
- defaultTelemetryCollector.recordCall(toolName, args, result);
104
- }
105
- }
106
- catch {
107
- // Fail-Open 非阻塞防线:严禁打点逻辑异常影响主工具调用
160
+ onHostEvent(ctx, 'tools/result', (exec, result) => {
161
+ try {
162
+ const toolName = exec?.name || exec?.tool?.name || '';
163
+ if (typeof toolName === 'string' && toolName.startsWith('github_')) {
164
+ const args = exec?.arguments || exec?.args || {};
165
+ defaultTelemetryCollector.recordCall(toolName, args, result);
108
166
  }
109
- });
110
- }
167
+ }
168
+ catch {
169
+ // Fail-Open 非阻塞防线:严禁打点逻辑异常影响主工具调用
170
+ }
171
+ });
111
172
  // 3. 注册人类 Slash 命令 (/gh status, /gh upgrade, /gh help)
112
- if (ctx.commands) {
113
- registerGhCommand(ctx.commands, executor, updateChecker, defaultTelemetryCollector);
114
- }
173
+ // 用 ctx.inject 声明式等待,而不是一次性 ctx.get 探测:服务若晚于本插件就绪,
174
+ // 一次性探测拿到的是 undefined,且**永远没有第二次机会**,/gh 就此消失。
175
+ // ctx.inject 的嵌套 fiber 在服务出现时补触发;服务始终不出现则它自己保持 PENDING,
176
+ // 外层插件照常 ACTIVE(18 个工具不受影响)。这就是「可选依赖」在 cordis 里的正确写法。
177
+ ctx.inject(['commands'], (c) => {
178
+ registerGhCommand(c.get('commands'), executor, updateChecker, defaultTelemetryCollector);
179
+ });
115
180
  // 4. 注册 Agent 模型工具集(核心领域工具 + 元指南)
116
- if (ctx.tools) {
117
- ctx.tools.register(createPrTool(executor));
118
- ctx.tools.register(createIssueTool(executor));
119
- ctx.tools.register(createRunTool(executor));
120
- ctx.tools.register(createRepoTool(executor));
121
- ctx.tools.register(createApiTool(executor));
122
- ctx.tools.register(createReleaseTool(executor));
123
- ctx.tools.register(createSecretTool(executor));
124
- ctx.tools.register(createSkillTool(executor));
125
- ctx.tools.register(createLabelTool(executor));
126
- ctx.tools.register(createRulesetTool(executor));
127
- ctx.tools.register(createCacheTool(executor));
128
- ctx.tools.register(createGistTool(executor));
129
- ctx.tools.register(createStatusTool(executor, defaultTelemetryCollector));
130
- ctx.tools.register(createAgentTaskTool(executor));
131
- ctx.tools.register(createDiscussionTool(executor));
132
- ctx.tools.register(createProjectTool(executor));
133
- ctx.tools.register(createCodespaceTool(executor));
134
- ctx.tools.register(createHelpTool(executor));
135
- }
136
- // 4. 注册面向 Client 端的 WebServer HTTP 路由(如果当前环境启用了 Web UI 服务)
137
- if (ctx.webServer) {
138
- // 传入 ctx 作用域对象,使 routes 能够通过 ctx.effect 绑定生命周期并在插件卸载/重载时自动注销;
139
- // cacheTtlMs 传给路由层以尊重用户的缓存 TTL 配置。
140
- // proxyManager 传入以挂载 /api/github/proxy* 设置路由(驾驶舱代理设置入口)。
141
- registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000, updateChecker, upgradeManager, proxyManager);
181
+ // `tools` 在 inject 内,运行期必然存在;其权威类型声明在 @deepseek-ai/dsh-tools,
182
+ // 本仓不重复 declare module(会与消费方的同一份声明冲突),故在此局部收窄。
183
+ const tools = ctx.tools;
184
+ // 先建全量数组再统一过档位与闸门:过滤点唯一,且 applyTier / wrapWithGate 都是纯函数,
185
+ // 可以完全离线单测(见 test/phase37-toolset / phase38-gate)。
186
+ const allTools = [
187
+ createPrTool(executor),
188
+ createIssueTool(executor),
189
+ createRunTool(executor),
190
+ createRepoTool(executor),
191
+ createApiTool(executor),
192
+ createReleaseTool(executor),
193
+ createSecretTool(executor),
194
+ createSkillTool(executor),
195
+ createLabelTool(executor),
196
+ createRulesetTool(executor),
197
+ createCacheTool(executor),
198
+ createGistTool(executor),
199
+ createStatusTool(executor, defaultTelemetryCollector),
200
+ createAgentTaskTool(executor),
201
+ createDiscussionTool(executor),
202
+ createProjectTool(executor),
203
+ createCodespaceTool(executor),
204
+ createHelpTool(executor),
205
+ // 任务级原语(s8):与 18 个领域工具并存,档位里用 core 预设二选一
206
+ createTaskTool(executor),
207
+ ];
208
+ const tier = applyTier(allTools, { toolsets: plain.toolsets, read_only: plain.read_only });
209
+ if (tier.dropped.length > 0) {
210
+ // 不静默:档位/只读模式剪掉的工具要能从日志里看到,否则"模型看不见某工具"无法排查
211
+ console.log(`[dsh-github-flow] 按档位配置未注册 ${tier.dropped.length} 个工具:${tier.dropped.join(', ')}`);
142
212
  }
143
- // 5. 启动后低优先级后台闲时静默探测:若有新版本,仅输出一次友好的控制台提示
144
- if (config.checkUpdate !== false) {
213
+ // 闸门已在各工具工厂内部经 wrapWithGate 生效,这里只做档位过滤后的注册。
214
+ for (const tool of tier.kept)
215
+ tools.register(tool);
216
+ // 5. 注册面向 Client 端的 WebServer HTTP 路由
217
+ // 同样用 ctx.inject 等待 webServer(理由同第 3 步)。
218
+ // 关键:绝不能在外层 `ctx.get('webServer')` 之后又让 routes 内部去读 `ctx.webServer`——
219
+ // 未 inject 时那个属性读会抛 `cannot get property "webServer" without inject`,
220
+ // 让 CLI/TUI profile 从「PENDING」变成「加载失败」。
221
+ // 这里把 webServer 显式作为末位参数传下去,routes 内部不再做属性读回退。
222
+ ctx.inject(['webServer'], (c) => {
223
+ registerApiRoutes(c, executor, plain.cacheTtlMs || 15_000, updateChecker, upgradeManager, proxyManager, c.get('webServer'));
224
+ });
225
+ // 6. volatile 字段热更新:loader 把新值提交进运行中的 fiber(不 reload),
226
+ // 这里把它推给执行引擎。事件由 @deepseek-ai/cordis-plugin-loader 派发,
227
+ // 只在「该字段确实声明了 .volatile()」时触发。
228
+ // 只推 GhExecutor.updateConfig 真正认的字段:认了不改的字段等于假热更新。
229
+ onHostEvent(ctx, 'loader/volatile-update', () => {
230
+ executor.updateConfig({
231
+ defaultTimeoutMs: config.defaultTimeoutMs.get(),
232
+ maxOutputChars: config.maxOutputChars.get(),
233
+ defaultListLimit: config.defaultListLimit.get(),
234
+ // maxOutputBytes 不是 volatile(它不该被单独热改),但**必须一起推**:
235
+ // updateConfig 收到 maxOutputChars 就会把 maxOutputBytes 重算成 ×4
236
+ // (executor.ts:218-222)。不带上用户显式配置的硬上限,一次热更新就会把它冲掉
237
+ // ——实测 {maxOutputChars:1000, maxOutputBytes:500} 会变成 4000。
238
+ maxOutputBytes: config.maxOutputBytes,
239
+ });
240
+ });
241
+ // 7. 启动后低优先级后台闲时静默探测:若有新版本,仅输出一次友好的控制台提示
242
+ if (plain.checkUpdate) {
145
243
  // 包裹在 ctx.effect 中(Cordis 规范):热重载/卸载发生在 3 秒延迟窗口内时,
146
244
  // 取消挂起的定时器,避免旧实例在新实例加载后仍向控制台打印升级横幅。
147
- // 无头/测试环境缺 ctx.effect 时优雅退化为裸定时器(一次性触发,无泄漏)。
148
- const scheduleProbe = () => {
245
+ ctx.effect(() => {
149
246
  const timer = setTimeout(() => {
150
247
  updateChecker.check(false).then((info) => {
151
248
  if (info.hasUpdate) {
@@ -160,13 +257,7 @@ export function apply(ctx, config = {}) {
160
257
  }).catch(() => { });
161
258
  }, 3_000);
162
259
  return () => clearTimeout(timer);
163
- };
164
- if (typeof ctx.effect === 'function') {
165
- ctx.effect(scheduleProbe);
166
- }
167
- else {
168
- scheduleProbe();
169
- }
260
+ });
170
261
  }
171
262
  }
172
263
  export * from './types.js';
@@ -1,6 +1,3 @@
1
- /**
2
- * DSH GitHub Flow - 通用 Markdown 格式化原子工具与状态徽章
3
- */
4
1
  /** 安全清洗表格单元格文本:转义竖线,压缩换行,限制最大长度 */
5
2
  export declare function sanitizeCell(text: any, maxLen?: number): string;
6
3
  /** 格式化 PR / Issue 状态药丸徽章 */
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * DSH GitHub Flow - 通用 Markdown 格式化原子工具与状态徽章
3
+ *
4
+ * 状态徽章的实现已下沉到 `src/shared/state-badges.ts`(与客户端 UI 共用同一份映射)。
5
+ * 这里保留 `format*Badge` 的函数名与签名,服务端 14 处调用点零改动。
3
6
  */
7
+ import { badgeText, prStateBadge, ciBadge, conflictBadge, releaseBadge, } from '../shared/state-badges.js';
4
8
  /** 安全清洗表格单元格文本:转义竖线,压缩换行,限制最大长度 */
5
9
  export function sanitizeCell(text, maxLen = 80) {
6
10
  if (text === null || text === undefined || text === '')
@@ -16,18 +20,7 @@ export function sanitizeCell(text, maxLen = 80) {
16
20
  }
17
21
  /** 格式化 PR / Issue 状态药丸徽章 */
18
22
  export function formatStateBadge(state, isDraft) {
19
- if (!state)
20
- return '-';
21
- const s = state.toUpperCase();
22
- if (isDraft)
23
- return '⚪ DRAFT';
24
- if (s === 'OPEN')
25
- return '🟢 OPEN';
26
- if (s === 'MERGED')
27
- return '🟣 MERGED';
28
- if (s === 'CLOSED')
29
- return '🔴 CLOSED';
30
- return s;
23
+ return badgeText(prStateBadge(state, isDraft));
31
24
  }
32
25
  /**
33
26
  * 格式化 CI 工作流状态徽章。
@@ -36,44 +29,15 @@ export function formatStateBadge(state, isDraft) {
36
29
  * 同一组徽章;state 参数兜底(in_progress/queued 等进行中态)。
37
30
  */
38
31
  export function formatCiBadge(status, conclusion) {
39
- const conc = (conclusion || '').toLowerCase();
40
- const st = (status || '').toLowerCase();
41
- if (conc === 'success' || conc === 'pass')
42
- return '✅ ' + conc;
43
- if (conc === 'failure' || conc === 'fail')
44
- return '❌ ' + conc;
45
- if (conc === 'cancelled' || conc === 'cancel')
46
- return '🚫 ' + conc;
47
- if (conc === 'skipped' || conc === 'skipping')
48
- return '⏭️ ' + conc;
49
- if (conc === 'pending')
50
- return '⏳ pending';
51
- if (st === 'in_progress')
52
- return '⏳ running';
53
- if (st === 'queued')
54
- return '⏱️ queued';
55
- return conc || st || '-';
32
+ return badgeText(ciBadge(status, conclusion));
56
33
  }
57
34
  /** 格式化合并冲突状态 */
58
35
  export function formatConflictBadge(status) {
59
- if (!status)
60
- return '-';
61
- const s = status.toUpperCase();
62
- if (s === 'CLEAN')
63
- return '✅ CLEAN';
64
- if (s === 'DIRTY' || s === 'CONFLICT')
65
- return '⚠️ CONFLICT';
66
- if (s === 'BLOCKED')
67
- return '⛔ BLOCKED';
68
- return s;
36
+ return badgeText(conflictBadge(status));
69
37
  }
70
38
  /** 格式化 Release 状态徽章 */
71
39
  export function formatReleaseBadge(isDraft, isPrerelease) {
72
- if (isDraft)
73
- return '⚪ DRAFT';
74
- if (isPrerelease)
75
- return '🍼 PRE';
76
- return '✅ STABLE';
40
+ return badgeText(releaseBadge(isDraft, isPrerelease));
77
41
  }
78
42
  /**
79
43
  * 降级链路可见标注(runGuarded 契约的渲染兑现)。
@@ -20,3 +20,4 @@ export * from './agent-task.js';
20
20
  export * from './discussion.js';
21
21
  export * from './project.js';
22
22
  export * from './codespace.js';
23
+ export * from './task.js';
@@ -20,3 +20,4 @@ export * from './agent-task.js';
20
20
  export * from './discussion.js';
21
21
  export * from './project.js';
22
22
  export * from './codespace.js';
23
+ export * from './task.js';
@@ -1,4 +1,5 @@
1
1
  import { sanitizeCell, formatStateBadge, formatCiBadge, formatConflictBadge } from './common.js';
2
+ import { renderReviewRules } from '../review-rules.js';
2
3
  /**
3
4
  * PR 领域渲染器:负责 PR 列表高密度表格、CI Checks、分支状态与单卡详情格式化
4
5
  */
@@ -89,17 +90,41 @@ export function renderPrMarkdown(args, value) {
89
90
  const link = value.url ? `[#${value.number}](${value.url})` : `#${value.number}`;
90
91
  const badge = formatStateBadge(value.state, value.isDraft);
91
92
  const author = value.author?.login ? `@${value.author.login}` : '-';
93
+ /*
94
+ * 分支名会移动(force-push / 新提交),拿它当审查锚点等于「我审的到底是哪一版」
95
+ * 永远说不清。`branch@a1b2c3d` 是 git 世界的通用写法:7 位足够人读,
96
+ * 完整 SHA 仍在 JSON 里(headRefOid),需要精确引用时取它。
97
+ * 老服务端 / 老 gh 没有 headRefOid 时只显示分支名,绝不渲染 `@undefined`。
98
+ */
99
+ const headSha = typeof value.headRefOid === 'string' && value.headRefOid.length >= 7
100
+ ? `@${value.headRefOid.slice(0, 7)}`
101
+ : '';
92
102
  const lines = [
93
103
  `### ${badge} ${value.title} (${link})`,
94
- `- **作者**: ${author} | **分支**: \`${value.headRefName || '-'}\` → \`${value.baseRefName || 'main'}\``,
104
+ `- **作者**: ${author} | **分支**: \`${value.headRefName || '-'}${headSha}\` → \`${value.baseRefName || 'main'}\``,
95
105
  `- **合并判定**: ${formatConflictBadge(value.mergeable || value.mergeStateStatus)} | **审查判定**: ${value.reviewDecision || 'NONE'}`,
96
106
  ];
97
107
  if (Array.isArray(value.labels) && value.labels.length > 0) {
98
108
  lines.push(`- **标签**: ${value.labels.map((l) => `\`${l.name || l}\``).join(' ')}`);
99
109
  }
110
+ // 清单截断必须让 Agent 也知道(人看 UI、Agent 看这里,同一事实两处都要说)
111
+ if (typeof value.changedFiles === 'number' && Array.isArray(value.files) && value.files.length < value.changedFiles) {
112
+ lines.push(`- **变动文件**: 仅列出前 ${value.files.length} / ${value.changedFiles} 个(gh 单次最多返回 100 条)`);
113
+ }
100
114
  if (value.body) {
101
115
  lines.push('\n**正文说明:**\n```markdown\n' + sanitizeCell(value.body, 300) + '\n```');
102
116
  }
117
+ /*
118
+ * 仓库审查规则(AGENTS.md):人看 UI、Agent 看这里,同一事实两处都要说。
119
+ * 只有工具确实注入时才渲染(`reviewRules` 键不存在 = 工作区没有规则文件,
120
+ * 此时不得输出一个空章节让人以为「规则为空」)。
121
+ *
122
+ * 渲染逻辑复用 `review-rules.ts` 的 `renderReviewRules`——同一条规则只能有
123
+ * 一处实现,否则「工具返回的 JSON」与「Markdown 输出」迟早会漂移。
124
+ */
125
+ const rulesMd = renderReviewRules(value.reviewRules);
126
+ if (rulesMd)
127
+ lines.push(rulesMd);
103
128
  return lines.join('\n');
104
129
  }
105
130
  // 兜底降级
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `github_task` 的 Markdown 渲染器。
3
+ *
4
+ * 为什么单独渲染而不是 JSON 直出:原语的返回值是「给模型看的结论包」,
5
+ * 其中 `cost` 与 `truncated` 是审计信息、`facts` 是结构化事实。
6
+ * 直接 JSON.stringify 会把最重要的 `summary` 埋在括号里;
7
+ * 而只渲染 summary 又会丢掉「这次花了多少」——两者都要,但要有主次。
8
+ */
9
+ export declare function renderTaskMarkdown(args: any, value: any): string;