dsh-github-router 0.1.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/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +80 -0
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/README.zh.md +156 -0
- package/SECURITY.md +112 -0
- package/cordis.patch.yml +8 -0
- package/docs/design.md +198 -0
- package/lib/cache.js +91 -0
- package/lib/client.js +553 -0
- package/lib/config.js +109 -0
- package/lib/core/file.js +166 -0
- package/lib/core/issue.js +202 -0
- package/lib/core/pr.js +429 -0
- package/lib/core/probe.js +121 -0
- package/lib/core/runtime.js +89 -0
- package/lib/guidance.js +24 -0
- package/lib/index.js +52 -0
- package/lib/net.js +213 -0
- package/lib/remote.js +169 -0
- package/lib/render.js +154 -0
- package/lib/routes/api.js +136 -0
- package/lib/routes/gh.js +109 -0
- package/lib/routes/git.js +429 -0
- package/lib/routes/html.js +250 -0
- package/lib/routes/mirror.js +55 -0
- package/lib/skill.js +44 -0
- package/lib/tools/api.js +101 -0
- package/lib/tools/file.js +48 -0
- package/lib/tools/issue.js +46 -0
- package/lib/tools/pr.js +56 -0
- package/lib/tools/probe.js +42 -0
- package/lib/tunnel.js +247 -0
- package/lib/util.js +235 -0
- package/package.json +79 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-github-router — client half (hand-written factory bundle, no build step).
|
|
3
|
+
*
|
|
4
|
+
* One INDEPENDENT settings page (Settings → "GitHub 路由"), registered into
|
|
5
|
+
* the `settings.section` slot exactly like dsh-notification does. The
|
|
6
|
+
* configuration channel is the dsh-market pattern: the Host mounts
|
|
7
|
+
* `/dsh-github-router/config` on the shared web server and this page talks
|
|
8
|
+
* to it with plain same-origin fetch (GET = redacted view, POST = staged
|
|
9
|
+
* writes with revision fencing). Edits are staged locally and written only
|
|
10
|
+
* on save; a rejected write reloads the view so the next save fences
|
|
11
|
+
* correctly.
|
|
12
|
+
*
|
|
13
|
+
* Layout: the common fields (token, proxy, route switches) sit up top; the
|
|
14
|
+
* long tail (timeouts, retries, cache TTLs, mirrors, repos, git cache dir)
|
|
15
|
+
* lives in a collapsed "Advanced settings" disclosure below.
|
|
16
|
+
*
|
|
17
|
+
* The module requires only `react` and
|
|
18
|
+
* `@deepseek-ai/dsh-client-runtime/client` (the browser subpath — the bare
|
|
19
|
+
* package name resolves to the Node half). All copy goes through the
|
|
20
|
+
* framework locale service under the `dsh-github-router` namespace.
|
|
21
|
+
*/
|
|
22
|
+
window.__ModuleLoader__.load({
|
|
23
|
+
id: 'dsh-github-router',
|
|
24
|
+
factory: (require) => {
|
|
25
|
+
var module = { exports: {} };
|
|
26
|
+
var exports = module.exports;
|
|
27
|
+
var React = require('react');
|
|
28
|
+
var runtime = require('@deepseek-ai/dsh-client-runtime/client');
|
|
29
|
+
|
|
30
|
+
// ------------------------------------------------------------ specs
|
|
31
|
+
// Flat scalar fields only: the client settings scope writes one field
|
|
32
|
+
// per call (`scope.set(field, value)`), so nested schema objects are
|
|
33
|
+
// deliberately absent from this page.
|
|
34
|
+
function textSpec(field) {
|
|
35
|
+
return {
|
|
36
|
+
field: field,
|
|
37
|
+
format: function (value) { return typeof value === 'string' ? value : ''; },
|
|
38
|
+
parse: function (text) {
|
|
39
|
+
var trimmed = String(text).trim();
|
|
40
|
+
return trimmed === '' ? { kind: 'clear' } : { kind: 'set', value: trimmed };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function numberSpec(field) {
|
|
45
|
+
return {
|
|
46
|
+
field: field,
|
|
47
|
+
format: function (value) { return typeof value === 'number' ? String(value) : ''; },
|
|
48
|
+
parse: function (text) {
|
|
49
|
+
var trimmed = String(text).trim();
|
|
50
|
+
if (trimmed === '') return { kind: 'clear' };
|
|
51
|
+
var parsed = Number(trimmed);
|
|
52
|
+
return Number.isFinite(parsed) ? { kind: 'set', value: parsed } : undefined;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function boolSpec(field) {
|
|
57
|
+
return {
|
|
58
|
+
field: field,
|
|
59
|
+
format: function (value) { return value === true ? 'true' : 'false'; },
|
|
60
|
+
parse: function (text) {
|
|
61
|
+
var trimmed = String(text).trim();
|
|
62
|
+
if (trimmed === '') return { kind: 'clear' };
|
|
63
|
+
if (trimmed === 'true') return { kind: 'set', value: true };
|
|
64
|
+
if (trimmed === 'false') return { kind: 'set', value: false };
|
|
65
|
+
return undefined;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function csvSpec(field) {
|
|
70
|
+
return {
|
|
71
|
+
field: field,
|
|
72
|
+
format: function (value) { return Array.isArray(value) ? value.join(', ') : ''; },
|
|
73
|
+
parse: function (text) {
|
|
74
|
+
var trimmed = String(text).trim();
|
|
75
|
+
if (trimmed === '') return { kind: 'clear' };
|
|
76
|
+
return {
|
|
77
|
+
kind: 'set',
|
|
78
|
+
value: trimmed.split(',').map(function (part) { return part.trim(); }).filter(function (part) { return part.length > 0; }),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ------------------------------------------------------ field catalog
|
|
85
|
+
var PRIMARY_FIELDS = [
|
|
86
|
+
{ field: 'token', kind: 'text', spec: textSpec('token'), labelKey: 'field.token', hintKey: 'hint.token' },
|
|
87
|
+
{ field: 'tokenEnv', kind: 'text', spec: textSpec('tokenEnv'), labelKey: 'field.tokenEnv', hintKey: 'hint.tokenEnv' },
|
|
88
|
+
{ field: 'proxy', kind: 'text', spec: textSpec('proxy'), labelKey: 'field.proxy', hintKey: 'hint.proxy' },
|
|
89
|
+
{ field: 'routesApi', kind: 'bool', spec: boolSpec('routesApi'), labelKey: 'field.routesApi', hintKey: null },
|
|
90
|
+
{ field: 'routesGh', kind: 'bool', spec: boolSpec('routesGh'), labelKey: 'field.routesGh', hintKey: null },
|
|
91
|
+
{ field: 'routesGit', kind: 'bool', spec: boolSpec('routesGit'), labelKey: 'field.routesGit', hintKey: null },
|
|
92
|
+
{ field: 'routesHtml', kind: 'bool', spec: boolSpec('routesHtml'), labelKey: 'field.routesHtml', hintKey: null },
|
|
93
|
+
];
|
|
94
|
+
var ADVANCED_FIELDS = [
|
|
95
|
+
{ field: 'directTimeoutMs', kind: 'number', spec: numberSpec('directTimeoutMs'), labelKey: 'field.directTimeoutMs', hintKey: null },
|
|
96
|
+
{ field: 'proxyTimeoutMs', kind: 'number', spec: numberSpec('proxyTimeoutMs'), labelKey: 'field.proxyTimeoutMs', hintKey: null },
|
|
97
|
+
{ field: 'retries', kind: 'number', spec: numberSpec('retries'), labelKey: 'field.retries', hintKey: null },
|
|
98
|
+
{ field: 'maxBytes', kind: 'number', spec: numberSpec('maxBytes'), labelKey: 'field.maxBytes', hintKey: 'hint.maxBytes' },
|
|
99
|
+
{ field: 'cacheTtlMeta', kind: 'number', spec: numberSpec('cacheTtlMeta'), labelKey: 'field.cacheTtlMeta', hintKey: 'hint.cacheTtlMeta' },
|
|
100
|
+
{ field: 'cacheTtlContent', kind: 'number', spec: numberSpec('cacheTtlContent'), labelKey: 'field.cacheTtlContent', hintKey: 'hint.cacheTtlContent' },
|
|
101
|
+
{ field: 'routesMirror', kind: 'bool', spec: boolSpec('routesMirror'), labelKey: 'field.routesMirror', hintKey: 'hint.routesMirror' },
|
|
102
|
+
{ field: 'mirrors', kind: 'text', spec: csvSpec('mirrors'), labelKey: 'field.mirrors', hintKey: 'hint.mirrors' },
|
|
103
|
+
{ field: 'repos', kind: 'text', spec: csvSpec('repos'), labelKey: 'field.repos', hintKey: 'hint.repos' },
|
|
104
|
+
{ field: 'gitCacheDir', kind: 'text', spec: textSpec('gitCacheDir'), labelKey: 'field.gitCacheDir', hintKey: 'hint.gitCacheDir' },
|
|
105
|
+
];
|
|
106
|
+
var FIELDS = PRIMARY_FIELDS.concat(ADVANCED_FIELDS);
|
|
107
|
+
var SPECS = {};
|
|
108
|
+
FIELDS.forEach(function (f) { SPECS[f.field] = f.spec; });
|
|
109
|
+
|
|
110
|
+
// ------------------------------------------------------------- i18n
|
|
111
|
+
var NS = 'dsh-github-router';
|
|
112
|
+
var zhDict = {
|
|
113
|
+
'nav': 'GitHub 路由',
|
|
114
|
+
'description': 'GitHub 读取的路由与缓存配置。',
|
|
115
|
+
'advanced': '高级设置',
|
|
116
|
+
'unavailable': '无法从宿主加载配置。',
|
|
117
|
+
'save': '保存',
|
|
118
|
+
'saving': '保存中…',
|
|
119
|
+
'discard': '放弃修改',
|
|
120
|
+
'failed': '保存失败,请重试。',
|
|
121
|
+
'readOnly': '当前部署不允许修改设置。',
|
|
122
|
+
'invalid': '无效值',
|
|
123
|
+
'overridden': '已覆盖',
|
|
124
|
+
'field.token': 'GitHub Token',
|
|
125
|
+
'field.tokenEnv': 'Token 环境变量',
|
|
126
|
+
'field.proxy': '代理地址',
|
|
127
|
+
'field.directTimeoutMs': '直连超时(毫秒)',
|
|
128
|
+
'field.proxyTimeoutMs': '代理超时(毫秒)',
|
|
129
|
+
'field.retries': '重试次数(429/5xx)',
|
|
130
|
+
'field.maxBytes': '响应字节上限',
|
|
131
|
+
'field.cacheTtlMeta': '元数据缓存(秒)',
|
|
132
|
+
'field.cacheTtlContent': '内容缓存(秒)',
|
|
133
|
+
'field.routesApi': 'api.github.com 路由',
|
|
134
|
+
'field.routesGh': 'gh CLI 路由',
|
|
135
|
+
'field.routesGit': 'git 协议路由',
|
|
136
|
+
'field.routesHtml': '页面 HTML 路由',
|
|
137
|
+
'field.routesMirror': '镜像路由(默认关闭)',
|
|
138
|
+
'field.mirrors': '镜像基址(逗号分隔)',
|
|
139
|
+
'field.repos': '本地仓库白名单(逗号分隔)',
|
|
140
|
+
'field.gitCacheDir': 'git 缓存目录',
|
|
141
|
+
'hint.token': '留空表示清除;优先使用 tokenEnv。',
|
|
142
|
+
'hint.tokenEnv': '指向 token 的环境变量名,例如 GITHUB_TOKEN。',
|
|
143
|
+
'hint.proxy': '空 = 继承环境代理;direct = 永不代理;其余为代理 URL。',
|
|
144
|
+
'hint.maxBytes': '每个响应体的字节上限(16384..8388608)。',
|
|
145
|
+
'hint.cacheTtlMeta': 'PR/issue 元数据缓存秒数(0 = 不缓存)。',
|
|
146
|
+
'hint.cacheTtlContent': '文件/提交/diff 等近似不可变内容的缓存秒数。',
|
|
147
|
+
'hint.routesMirror': '镜像为第三方,会看到请求路径;仅在知情时开启。',
|
|
148
|
+
'hint.mirrors': '例如 https://ghproxy.net;多个用逗号分隔。',
|
|
149
|
+
'hint.repos': '授权只读 git 路由读取的本地仓库路径(仅 log/diff/show)。',
|
|
150
|
+
'hint.gitCacheDir': '空 = <DSH_HOME>/storages/dsh-github-router/git。',
|
|
151
|
+
};
|
|
152
|
+
var enDict = {
|
|
153
|
+
'nav': 'GitHub Router',
|
|
154
|
+
'description': 'Routing and cache configuration for GitHub reads.',
|
|
155
|
+
'advanced': 'Advanced settings',
|
|
156
|
+
'unavailable': 'The configuration could not be loaded from the host.',
|
|
157
|
+
'save': 'Save',
|
|
158
|
+
'saving': 'Saving…',
|
|
159
|
+
'discard': 'Discard',
|
|
160
|
+
'failed': 'The save failed. Please retry.',
|
|
161
|
+
'readOnly': 'This deployment does not allow settings edits.',
|
|
162
|
+
'invalid': 'invalid',
|
|
163
|
+
'overridden': 'overridden',
|
|
164
|
+
'field.token': 'GitHub token',
|
|
165
|
+
'field.tokenEnv': 'Token env var',
|
|
166
|
+
'field.proxy': 'Proxy URL',
|
|
167
|
+
'field.directTimeoutMs': 'Direct timeout (ms)',
|
|
168
|
+
'field.proxyTimeoutMs': 'Proxy timeout (ms)',
|
|
169
|
+
'field.retries': 'Retries (429/5xx)',
|
|
170
|
+
'field.maxBytes': 'Response byte cap',
|
|
171
|
+
'field.cacheTtlMeta': 'Metadata cache (s)',
|
|
172
|
+
'field.cacheTtlContent': 'Content cache (s)',
|
|
173
|
+
'field.routesApi': 'api.github.com route',
|
|
174
|
+
'field.routesGh': 'gh CLI route',
|
|
175
|
+
'field.routesGit': 'git protocol route',
|
|
176
|
+
'field.routesHtml': 'Page HTML route',
|
|
177
|
+
'field.routesMirror': 'Mirror route (off by default)',
|
|
178
|
+
'field.mirrors': 'Mirror bases (comma-separated)',
|
|
179
|
+
'field.repos': 'Local repo allowlist (comma-separated)',
|
|
180
|
+
'field.gitCacheDir': 'Git cache directory',
|
|
181
|
+
'hint.token': 'Blank clears the value; prefer tokenEnv.',
|
|
182
|
+
'hint.tokenEnv': 'Environment variable naming the token, e.g. GITHUB_TOKEN.',
|
|
183
|
+
'hint.proxy': "Empty = inherit ambient proxy env; 'direct' = never proxy; anything else is a proxy URL.",
|
|
184
|
+
'hint.maxBytes': 'Byte cap per response body (16384..8388608).',
|
|
185
|
+
'hint.cacheTtlMeta': 'Cache seconds for PR/issue metadata (0 = no cache).',
|
|
186
|
+
'hint.cacheTtlContent': 'Cache seconds for near-immutable content (files, commits, diffs).',
|
|
187
|
+
'hint.routesMirror': 'Mirrors are third parties that see requested paths; enable only knowingly.',
|
|
188
|
+
'hint.mirrors': 'E.g. https://ghproxy.net; separate several with commas.',
|
|
189
|
+
'hint.repos': 'Local repositories granted for read-only git-route reads (log/diff/show only).',
|
|
190
|
+
'hint.gitCacheDir': 'Empty = <DSH_HOME>/storages/dsh-github-router/git.',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// ------------------------------------------------------------- form
|
|
194
|
+
/**
|
|
195
|
+
* The configuration channel is the dsh-market pattern: the Host mounts
|
|
196
|
+
* `/dsh-github-router/config` on the shared web server, and this page
|
|
197
|
+
* uses plain same-origin fetch (GET = redacted view, POST = writes).
|
|
198
|
+
* The framework's settings scope is NOT used — its api-proxy serves
|
|
199
|
+
* settings namespaces through a hardcoded allowlist that third-party
|
|
200
|
+
* plugins cannot join.
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
/** One `/dsh-github-router/config` call (GET or POST). */
|
|
204
|
+
function configCall(payload) {
|
|
205
|
+
var init = payload === undefined
|
|
206
|
+
? { method: 'GET', cache: 'no-store' }
|
|
207
|
+
: {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
cache: 'no-store',
|
|
210
|
+
headers: { 'content-type': 'application/json' },
|
|
211
|
+
body: JSON.stringify(payload),
|
|
212
|
+
};
|
|
213
|
+
return fetch('/dsh-github-router/config', init).then(function (response) {
|
|
214
|
+
return response.json().then(function (envelope) {
|
|
215
|
+
if (!envelope || envelope.ok !== true) {
|
|
216
|
+
var message = envelope && envelope.error !== undefined ? String(envelope.error) : 'config call failed (HTTP ' + response.status + ')';
|
|
217
|
+
throw new Error('dsh-github-router: ' + message);
|
|
218
|
+
}
|
|
219
|
+
return envelope.value;
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Scope adapter over the plugin routes: same interface the form uses. */
|
|
225
|
+
function RemoteScope() {
|
|
226
|
+
var listeners = new Set();
|
|
227
|
+
var snapshot = { status: 'loading', value: {}, base: {}, user: {}, revision: undefined, writable: false, mode: 'host' };
|
|
228
|
+
|
|
229
|
+
function publish() {
|
|
230
|
+
listeners.forEach(function (fn) { fn(); });
|
|
231
|
+
}
|
|
232
|
+
function accept(view) {
|
|
233
|
+
snapshot = {
|
|
234
|
+
status: 'ready',
|
|
235
|
+
value: view && typeof view.value === 'object' ? view.value : {},
|
|
236
|
+
base: view && typeof view.base === 'object' ? view.base : {},
|
|
237
|
+
user: view && typeof view.user === 'object' ? view.user : {},
|
|
238
|
+
revision: view && view.revision,
|
|
239
|
+
writable: !!(view && view.writable),
|
|
240
|
+
mode: 'host',
|
|
241
|
+
};
|
|
242
|
+
publish();
|
|
243
|
+
}
|
|
244
|
+
function fail(message) {
|
|
245
|
+
snapshot = { status: 'unavailable', value: {}, base: {}, user: {}, revision: undefined, writable: false, mode: 'memory' };
|
|
246
|
+
console.error('[dsh-github-router] config load failed', message);
|
|
247
|
+
publish();
|
|
248
|
+
}
|
|
249
|
+
function load() {
|
|
250
|
+
return configCall().then(accept, fail);
|
|
251
|
+
}
|
|
252
|
+
function write(op) {
|
|
253
|
+
return configCall({ ops: [op], expectedRevision: snapshot.revision }).then(accept, function (error) {
|
|
254
|
+
// A rejected write may mean the document moved (revision conflict)
|
|
255
|
+
// or storage failed — re-read so the next save fences correctly.
|
|
256
|
+
return load().then(function () { throw error; });
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
load();
|
|
261
|
+
return {
|
|
262
|
+
getSnapshot: function () { return snapshot; },
|
|
263
|
+
subscribe: function (fn) {
|
|
264
|
+
listeners.add(fn);
|
|
265
|
+
return function () { listeners.delete(fn); };
|
|
266
|
+
},
|
|
267
|
+
set: function (field, value) {
|
|
268
|
+
return write({ op: 'set', path: [field], value: value });
|
|
269
|
+
},
|
|
270
|
+
unset: function (field) {
|
|
271
|
+
return write({ op: 'unset', path: [field] });
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** A static scope stub for environments without fetch. */
|
|
277
|
+
function unavailableScope() {
|
|
278
|
+
var snapshot = { status: 'unavailable', value: {}, base: {}, user: {}, revision: undefined, writable: false, mode: 'memory' };
|
|
279
|
+
return {
|
|
280
|
+
getSnapshot: function () { return snapshot; },
|
|
281
|
+
subscribe: function () { return function () {}; },
|
|
282
|
+
set: function () { return Promise.resolve(); },
|
|
283
|
+
unset: function () { return Promise.resolve(); },
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function GithubRouterForm(scope) {
|
|
288
|
+
var staged = new Map(); // field -> { text, clear }
|
|
289
|
+
var saving = false;
|
|
290
|
+
var failed = false;
|
|
291
|
+
var store = runtime.createSnapshotStore({
|
|
292
|
+
available: false,
|
|
293
|
+
writable: false,
|
|
294
|
+
dirty: false,
|
|
295
|
+
invalid: false,
|
|
296
|
+
saving: false,
|
|
297
|
+
failed: false,
|
|
298
|
+
fields: {},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
function snap() { return scope.getSnapshot(); }
|
|
302
|
+
function sectionValue(field) {
|
|
303
|
+
var s = snap();
|
|
304
|
+
return s.value === undefined || s.value === null ? undefined : s.value[field];
|
|
305
|
+
}
|
|
306
|
+
function baseValue(field) {
|
|
307
|
+
var s = snap();
|
|
308
|
+
return s.base === undefined || s.base === null ? undefined : s.base[field];
|
|
309
|
+
}
|
|
310
|
+
function stored(field) {
|
|
311
|
+
var s = snap();
|
|
312
|
+
return !!(s.user && typeof s.user === 'object' && Object.prototype.hasOwnProperty.call(s.user, field));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function publish() {
|
|
316
|
+
var s = snap();
|
|
317
|
+
var fields = {};
|
|
318
|
+
var invalid = false;
|
|
319
|
+
FIELDS.forEach(function (f) {
|
|
320
|
+
var stagedEntry = staged.get(f.field);
|
|
321
|
+
var text;
|
|
322
|
+
var overridden;
|
|
323
|
+
var invalidField = false;
|
|
324
|
+
if (stagedEntry !== undefined) {
|
|
325
|
+
text = stagedEntry.text;
|
|
326
|
+
overridden = !stagedEntry.clear;
|
|
327
|
+
if (!stagedEntry.clear) invalidField = f.spec.parse(stagedEntry.text) === undefined;
|
|
328
|
+
} else {
|
|
329
|
+
text = f.spec.format(sectionValue(f.field));
|
|
330
|
+
overridden = stored(f.field);
|
|
331
|
+
}
|
|
332
|
+
if (invalidField) invalid = true;
|
|
333
|
+
fields[f.field] = { text: text, overridden: overridden, invalid: invalidField };
|
|
334
|
+
});
|
|
335
|
+
store.set({
|
|
336
|
+
available: s.status === 'ready',
|
|
337
|
+
writable: !!s.writable,
|
|
338
|
+
dirty: staged.size > 0,
|
|
339
|
+
invalid: invalid,
|
|
340
|
+
saving: saving,
|
|
341
|
+
failed: failed,
|
|
342
|
+
fields: fields,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
scope.subscribe(publish);
|
|
346
|
+
publish();
|
|
347
|
+
|
|
348
|
+
function edit(field, text) {
|
|
349
|
+
staged.set(field, { text: String(text), clear: false });
|
|
350
|
+
failed = false;
|
|
351
|
+
publish();
|
|
352
|
+
}
|
|
353
|
+
function resetField(field) {
|
|
354
|
+
staged.set(field, { text: SPECS[field].format(baseValue(field)), clear: true });
|
|
355
|
+
failed = false;
|
|
356
|
+
publish();
|
|
357
|
+
}
|
|
358
|
+
function discard() {
|
|
359
|
+
if (staged.size === 0 && !failed) return;
|
|
360
|
+
staged.clear();
|
|
361
|
+
failed = false;
|
|
362
|
+
publish();
|
|
363
|
+
}
|
|
364
|
+
function save() {
|
|
365
|
+
if (saving || staged.size === 0) return;
|
|
366
|
+
var writes = [];
|
|
367
|
+
var blocked = false;
|
|
368
|
+
staged.forEach(function (stagedEntry, field) {
|
|
369
|
+
var spec = SPECS[field];
|
|
370
|
+
if (stagedEntry.clear) {
|
|
371
|
+
if (stored(field)) {
|
|
372
|
+
writes.push({ field: field, run: function () { return scope.unset(field); } });
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (stagedEntry.text === spec.format(sectionValue(field))) return;
|
|
377
|
+
var write = spec.parse(stagedEntry.text);
|
|
378
|
+
if (write === undefined) {
|
|
379
|
+
blocked = true;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (write.kind === 'clear') {
|
|
383
|
+
writes.push({ field: field, run: function () { return scope.unset(field); } });
|
|
384
|
+
} else {
|
|
385
|
+
writes.push({ field: field, run: (function (value) { return function () { return scope.set(field, value); }; })(write.value) });
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
if (blocked || writes.length === 0) return;
|
|
389
|
+
saving = true;
|
|
390
|
+
failed = false;
|
|
391
|
+
publish();
|
|
392
|
+
var landed = true;
|
|
393
|
+
var chain = Promise.resolve();
|
|
394
|
+
writes.forEach(function (w) {
|
|
395
|
+
chain = chain.then(function () {
|
|
396
|
+
return w.run().catch(function () { landed = false; });
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
chain.then(function () {
|
|
400
|
+
if (landed) staged.clear();
|
|
401
|
+
saving = false;
|
|
402
|
+
failed = !landed;
|
|
403
|
+
publish();
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function inject(t) {
|
|
408
|
+
return {
|
|
409
|
+
// The settings section shell synthesizes a `use<Key>` hook for
|
|
410
|
+
// every entry of this `hooks` object (dsh-notification's
|
|
411
|
+
// `hooks: { settings }` → `useSettings` is the working example).
|
|
412
|
+
hooks: { settings: store },
|
|
413
|
+
t: t,
|
|
414
|
+
edit: edit,
|
|
415
|
+
resetField: resetField,
|
|
416
|
+
save: save,
|
|
417
|
+
discard: discard,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return { inject: inject, store: store, save: save, edit: edit, discard: discard, resetField: resetField };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ------------------------------------------------------------- page
|
|
424
|
+
var pageStyle = { padding: '4px 0 24px' };
|
|
425
|
+
var gridStyle = {
|
|
426
|
+
display: 'grid',
|
|
427
|
+
gridTemplateColumns: 'minmax(180px, 1fr) minmax(180px, 1fr)',
|
|
428
|
+
gap: '8px 16px',
|
|
429
|
+
};
|
|
430
|
+
var rowStyle = { display: 'flex', flexDirection: 'column', gap: '2px', fontSize: '13px' };
|
|
431
|
+
var labelStyle = { color: 'var(--color-text-secondary, #666)' };
|
|
432
|
+
var inputStyle = {
|
|
433
|
+
padding: '4px 6px',
|
|
434
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
435
|
+
borderRadius: '4px',
|
|
436
|
+
background: 'var(--color-bg-input, transparent)',
|
|
437
|
+
color: 'inherit',
|
|
438
|
+
};
|
|
439
|
+
var boolRowStyle = { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' };
|
|
440
|
+
var footerStyle = { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '12px' };
|
|
441
|
+
var buttonStyle = {
|
|
442
|
+
padding: '4px 12px',
|
|
443
|
+
border: '1px solid var(--color-border, #ccc)',
|
|
444
|
+
borderRadius: '4px',
|
|
445
|
+
background: 'var(--color-bg-button, transparent)',
|
|
446
|
+
color: 'inherit',
|
|
447
|
+
cursor: 'pointer',
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
function GithubRouterSection(props) {
|
|
451
|
+
var state = props.useSettings(function (s) { return s; });
|
|
452
|
+
var t = props.t;
|
|
453
|
+
|
|
454
|
+
function renderField(f) {
|
|
455
|
+
var fieldState = state.fields[f.field] || { text: '', overridden: false, invalid: false };
|
|
456
|
+
if (f.kind === 'bool') {
|
|
457
|
+
return React.createElement(
|
|
458
|
+
'label',
|
|
459
|
+
{ key: f.field, style: boolRowStyle, title: f.hintKey ? t(f.hintKey) : undefined },
|
|
460
|
+
React.createElement('input', {
|
|
461
|
+
type: 'checkbox',
|
|
462
|
+
checked: fieldState.text === 'true',
|
|
463
|
+
disabled: !state.writable,
|
|
464
|
+
onChange: function (event) { props.edit(f.field, event.target.checked ? 'true' : 'false'); },
|
|
465
|
+
}),
|
|
466
|
+
t(f.labelKey),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return React.createElement(
|
|
470
|
+
'label',
|
|
471
|
+
{ key: f.field, style: rowStyle, title: f.hintKey ? t(f.hintKey) : undefined },
|
|
472
|
+
React.createElement('span', { style: labelStyle },
|
|
473
|
+
t(f.labelKey),
|
|
474
|
+
fieldState.overridden ? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, t('overridden')) : null,
|
|
475
|
+
),
|
|
476
|
+
React.createElement('input', {
|
|
477
|
+
type: f.kind === 'number' ? 'number' : 'text',
|
|
478
|
+
value: fieldState.text,
|
|
479
|
+
disabled: !state.writable,
|
|
480
|
+
style: inputStyle,
|
|
481
|
+
onChange: function (event) { props.edit(f.field, event.target.value); },
|
|
482
|
+
}),
|
|
483
|
+
fieldState.invalid ? React.createElement('span', { style: { color: '#c00' } }, t('invalid')) : null,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return React.createElement(
|
|
488
|
+
'section',
|
|
489
|
+
{ style: pageStyle },
|
|
490
|
+
React.createElement('p', { style: { margin: '0 0 12px', opacity: 0.75, fontSize: '13px' } }, t('description')),
|
|
491
|
+
!state.available ? React.createElement('div', { style: { color: '#c00', fontSize: '13px' } }, t('unavailable')) : null,
|
|
492
|
+
state.available && !state.writable ? React.createElement('div', { style: { color: '#c00', fontSize: '13px' } }, t('readOnly')) : null,
|
|
493
|
+
state.available ? React.createElement('div', { style: gridStyle }, PRIMARY_FIELDS.map(renderField)) : null,
|
|
494
|
+
state.available ? React.createElement(
|
|
495
|
+
'details',
|
|
496
|
+
{ style: { marginTop: '12px' } },
|
|
497
|
+
React.createElement('summary', { style: { cursor: 'pointer', fontSize: '13px', opacity: 0.85 } }, t('advanced')),
|
|
498
|
+
React.createElement('div', { style: Object.assign({ marginTop: '8px' }, gridStyle) }, ADVANCED_FIELDS.map(renderField)),
|
|
499
|
+
) : null,
|
|
500
|
+
state.available ? React.createElement('footer', { style: footerStyle },
|
|
501
|
+
React.createElement('button', {
|
|
502
|
+
style: buttonStyle,
|
|
503
|
+
disabled: !state.dirty || state.invalid || state.saving,
|
|
504
|
+
onClick: props.save,
|
|
505
|
+
}, state.saving ? t('saving') : t('save')),
|
|
506
|
+
React.createElement('button', {
|
|
507
|
+
style: buttonStyle,
|
|
508
|
+
disabled: !state.dirty || state.saving,
|
|
509
|
+
onClick: props.discard,
|
|
510
|
+
}, t('discard')),
|
|
511
|
+
state.failed ? React.createElement('span', { style: { color: '#c00', fontSize: '13px' } }, t('failed')) : null,
|
|
512
|
+
) : null,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ------------------------------------------------------------- plugin
|
|
517
|
+
var name = 'dsh-github-router';
|
|
518
|
+
var inject = ['slots', 'locale'];
|
|
519
|
+
function apply(ctx) {
|
|
520
|
+
console.info('[dsh-github-router] client bundle loaded');
|
|
521
|
+
try {
|
|
522
|
+
var t = ctx.locale.bind(NS);
|
|
523
|
+
ctx.effect(function () {
|
|
524
|
+
ctx.locale.register(NS, { zh: zhDict, en: enDict });
|
|
525
|
+
}, 'dsh-github-router: locale dictionaries');
|
|
526
|
+
var form = null;
|
|
527
|
+
ctx.slots.inject('settings.section', function* () {
|
|
528
|
+
yield ctx.slots.register({
|
|
529
|
+
name: 'settings.section',
|
|
530
|
+
id: 'dsh-github-router',
|
|
531
|
+
order: 65,
|
|
532
|
+
label: function () { return t('nav'); },
|
|
533
|
+
locale: NS,
|
|
534
|
+
inject: function () {
|
|
535
|
+
if (form === null) {
|
|
536
|
+
var scope = typeof fetch === 'function' ? new RemoteScope() : unavailableScope();
|
|
537
|
+
form = new GithubRouterForm(scope);
|
|
538
|
+
}
|
|
539
|
+
return form.inject(t);
|
|
540
|
+
},
|
|
541
|
+
}, GithubRouterSection);
|
|
542
|
+
});
|
|
543
|
+
console.info('[dsh-github-router] settings section registered');
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error('[dsh-github-router] client apply failed', error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
exports.apply = apply;
|
|
550
|
+
exports.inject = inject;
|
|
551
|
+
return module.exports;
|
|
552
|
+
},
|
|
553
|
+
});
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings schema and runtime-option resolution for dsh-github-router.
|
|
3
|
+
*
|
|
4
|
+
* The settings section (`dsh-github-router` namespace) is registered through
|
|
5
|
+
* `installSettingsSection`; the Settings UI renders a plugin card for it.
|
|
6
|
+
* Secrets are declared with `.role('secret')` so they are redacted on every
|
|
7
|
+
* wire boundary and rendered as write-only inputs.
|
|
8
|
+
*
|
|
9
|
+
* The schema is deliberately FLAT: the client settings scope writes scalar
|
|
10
|
+
* fields by name (`scope.set(field, value)`), so nested objects cannot be
|
|
11
|
+
* edited from the card. `resolveOptions` projects the flat section into the
|
|
12
|
+
* nested runtime shape the core aggregators use.
|
|
13
|
+
* @module dsh-github-router/config
|
|
14
|
+
*/
|
|
15
|
+
import z from '@deepseek-ai/schemastery'
|
|
16
|
+
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
17
|
+
|
|
18
|
+
/** Settings namespace carrying the plugin's configuration. */
|
|
19
|
+
export const NAMESPACE = settingsNamespace('dsh-github-router')
|
|
20
|
+
|
|
21
|
+
/** Composition/settings schema. All fields optional with safe defaults. */
|
|
22
|
+
export const Config = z.object({
|
|
23
|
+
/** Literal GitHub token (redacted on wire). Prefer tokenEnv. */
|
|
24
|
+
token: z.string().role('secret'),
|
|
25
|
+
/** Environment variable / credential ref naming the token. */
|
|
26
|
+
tokenEnv: z.string().role('credential-ref').default('GITHUB_TOKEN'),
|
|
27
|
+
/** Proxy URL for proxy routes. '' = inherit ambient proxy env; 'direct' = never proxy. */
|
|
28
|
+
proxy: z.string().default(''),
|
|
29
|
+
directTimeoutMs: z.number().min(1000).max(60000).default(8000),
|
|
30
|
+
proxyTimeoutMs: z.number().min(1000).max(60000).default(15000),
|
|
31
|
+
/** Retries for idempotent GETs on 429/5xx. */
|
|
32
|
+
retries: z.number().min(0).max(3).default(1),
|
|
33
|
+
routesApi: z.boolean().default(true),
|
|
34
|
+
routesGh: z.boolean().default(true),
|
|
35
|
+
routesGit: z.boolean().default(true),
|
|
36
|
+
routesHtml: z.boolean().default(true),
|
|
37
|
+
/** Raw-content mirrors: OFF by default — the user must opt in. */
|
|
38
|
+
routesMirror: z.boolean().default(false),
|
|
39
|
+
/** Raw-content mirror base URLs, e.g. ["https://ghproxy.net"]. Empty = none. */
|
|
40
|
+
mirrors: z.array(z.string()).default([]),
|
|
41
|
+
/** PR/issue metadata cache TTL. */
|
|
42
|
+
cacheTtlMeta: z.number().min(0).default(300),
|
|
43
|
+
/** Immutable-ish content (files, commits, diffs at a sha) cache TTL. */
|
|
44
|
+
cacheTtlContent: z.number().min(0).default(86400),
|
|
45
|
+
/** Cap for every response body read by this plugin, in bytes. */
|
|
46
|
+
maxBytes: z.number().min(16384).max(8388608).default(1048576),
|
|
47
|
+
/** Local repo paths granted for read-only git-route reads (log/diff/show only). */
|
|
48
|
+
repos: z.array(z.string()).default([]),
|
|
49
|
+
/** Plugin-owned git fetch cache dir. '' = <DSH_HOME>/storages/dsh-github-router/git. */
|
|
50
|
+
gitCacheDir: z.string().default(''),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const PROXY_ENV_NAMES = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy']
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve one settings/composition section into fully-defaulted runtime
|
|
57
|
+
* options. A thunk-producing source is consumed at call time so a settings
|
|
58
|
+
* change applies to the NEXT tool call.
|
|
59
|
+
* @param section - the resolved settings section (already schema-defaulted).
|
|
60
|
+
*/
|
|
61
|
+
export function resolveOptions(section) {
|
|
62
|
+
const s = section ?? {}
|
|
63
|
+
const proxy =
|
|
64
|
+
typeof s.proxy === 'string' && s.proxy.trim().length > 0
|
|
65
|
+
? s.proxy.trim()
|
|
66
|
+
: undefined
|
|
67
|
+
const routes = {
|
|
68
|
+
api: s.routesApi !== false,
|
|
69
|
+
gh: s.routesGh !== false,
|
|
70
|
+
git: s.routesGit !== false,
|
|
71
|
+
html: s.routesHtml !== false,
|
|
72
|
+
mirror: s.routesMirror === true,
|
|
73
|
+
}
|
|
74
|
+
const mirrors = Array.isArray(s.mirrors)
|
|
75
|
+
? s.mirrors.filter((m) => typeof m === 'string' && m.trim().length > 0).map((m) => m.trim())
|
|
76
|
+
: []
|
|
77
|
+
const repos = Array.isArray(s.repos)
|
|
78
|
+
? s.repos.filter((r) => typeof r === 'string' && r.trim().length > 0).map((r) => r.trim())
|
|
79
|
+
: []
|
|
80
|
+
return {
|
|
81
|
+
token: typeof s.token === 'string' && s.token.length > 0 ? s.token : undefined,
|
|
82
|
+
tokenEnv: typeof s.tokenEnv === 'string' && s.tokenEnv.length > 0 ? s.tokenEnv : 'GITHUB_TOKEN',
|
|
83
|
+
proxy, // undefined = ambient env decides; 'direct' = never proxy
|
|
84
|
+
directTimeoutMs: Number.isFinite(s.directTimeoutMs) ? s.directTimeoutMs : 8000,
|
|
85
|
+
proxyTimeoutMs: Number.isFinite(s.proxyTimeoutMs) ? s.proxyTimeoutMs : 15000,
|
|
86
|
+
retries: Number.isFinite(s.retries) ? Math.min(3, Math.max(0, s.retries)) : 1,
|
|
87
|
+
routes,
|
|
88
|
+
mirrors,
|
|
89
|
+
cacheTtlSeconds: {
|
|
90
|
+
meta: Number.isFinite(s.cacheTtlMeta) ? s.cacheTtlMeta : 300,
|
|
91
|
+
content: Number.isFinite(s.cacheTtlContent) ? s.cacheTtlContent : 86400,
|
|
92
|
+
},
|
|
93
|
+
maxBytes: Number.isFinite(s.maxBytes) ? Math.min(8388608, Math.max(16384, s.maxBytes)) : 1048576,
|
|
94
|
+
repos,
|
|
95
|
+
gitCacheDir: typeof s.gitCacheDir === 'string' && s.gitCacheDir.trim().length > 0 ? s.gitCacheDir.trim() : undefined,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The proxy URL one route attempt uses: explicit override > ambient env > none. */
|
|
100
|
+
export function effectiveProxy(options, explicit) {
|
|
101
|
+
if (explicit !== undefined && explicit !== null && explicit !== '') return explicit
|
|
102
|
+
if (options.proxy === 'direct') return undefined
|
|
103
|
+
if (options.proxy !== undefined && options.proxy !== '') return options.proxy
|
|
104
|
+
for (const name of PROXY_ENV_NAMES) {
|
|
105
|
+
const value = process.env[name]
|
|
106
|
+
if (value && value.length > 0) return value
|
|
107
|
+
}
|
|
108
|
+
return undefined
|
|
109
|
+
}
|