dsh-jira-tasks 1.0.5 → 1.0.6
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 +9 -0
- package/README.md +9 -0
- package/lib/client.js +53 -1
- package/lib/index.js +92 -7
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -11,6 +11,7 @@ Shows the current JIRA project's **open / reopened** issues **assigned to the cu
|
|
|
11
11
|
- 📋 Panel shown below the composer in both new and active sessions (aligned with the input width in new sessions)
|
|
12
12
|
- 👤 Defaults to the current user (`assignee = currentUser()`) with status `开启 / 重新开启` (Open / Reopened)
|
|
13
13
|
- ⚙️ **Settings → Plugins → JIRA** edits the base URL and access token (the token is written to the credential store and never sent back to the browser)
|
|
14
|
+
- 🟢 The card auto-probes the connection and shows a status light: green = usable, red = unusable, grey = unconfigured; **Test connection** verifies unsaved drafts immediately
|
|
14
15
|
- ⚙️ Project key and JQL are saved per workspace; unconfigured workspaces show "unconfigured"
|
|
15
16
|
- 🔄 Auto-query on every new session, with a one-click refresh (⟳)
|
|
16
17
|
- 🔗 Click an issue to open its JIRA detail in a new tab
|
|
@@ -56,6 +57,14 @@ Open **Settings → Plugins → Plugin configuration → JIRA** and fill in:
|
|
|
56
57
|
|
|
57
58
|
Leaving the token blank on save keeps the existing one; clearing the address on save removes the override and falls back to the environment. Auth is auto-detected: tokens containing `:` use Basic, otherwise Bearer (JIRA PAT).
|
|
58
59
|
|
|
60
|
+
#### Connection test
|
|
61
|
+
|
|
62
|
+
The card's footer carries a status light and a **Test connection** button:
|
|
63
|
+
|
|
64
|
+
- Opening the card **auto-probes** once (against JIRA `/rest/api/2/myself`), and saving re-probes
|
|
65
|
+
- **Green** = address and token work (the current user is shown); **red** = unusable (JIRA's reason, e.g. 401, is shown); **grey** = address or token not configured
|
|
66
|
+
- **Test connection** probes what is currently in the fields, saved or not, so you can check before saving
|
|
67
|
+
|
|
59
68
|
Environment variables / credentials still work as a **fallback** (used when the settings card is empty), hot-reloaded without a restart:
|
|
60
69
|
|
|
61
70
|
```yaml
|
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
- 👤 默认仅显示当前用户(`assignee = currentUser()`)的「开启 / 重新开启」任务
|
|
17
17
|
- ⚙️ **设置 → 插件 → JIRA** 配置 JIRA 地址与访问令牌(令牌写入凭据存储,不回传前端)
|
|
18
|
+
- 🟢 设置卡片自动探测连接并显示状态灯:绿=可用、红=不可用、灰=未配置;点「测试连接」可用**未保存的草稿值**即时验证
|
|
18
19
|
- ⚙️ 项目 Key 与 JQL 按工作区保存;未配置的工作区显示"未配置"
|
|
19
20
|
- 🔄 打开新会话自动查询,面板内支持一键刷新(⟳)
|
|
20
21
|
- 🧭 点击标题可**收起 / 展开**面板;标题徽标显示查询命中的任务总数(列表最多展示 50 条,按更新时间倒序)
|
|
@@ -64,6 +65,14 @@ dsh plugin --profile web add dsh-jira-tasks
|
|
|
64
65
|
|
|
65
66
|
留空并保存会保持已有令牌不变;地址留空并保存则清除设置项,回退到环境变量。认证自动识别:令牌含 `:` 用 Basic,否则用 Bearer(JIRA PAT)。
|
|
66
67
|
|
|
68
|
+
#### 连接测试
|
|
69
|
+
|
|
70
|
+
卡片底部有连接状态灯与 **测试连接** 按钮:
|
|
71
|
+
|
|
72
|
+
- 打开卡片时会**自动探测**一次(请求 JIRA `/rest/api/2/myself`),保存后也会自动重测
|
|
73
|
+
- **绿** = 地址与令牌可用(并显示当前登录用户);**红** = 不可用(显示 JIRA 返回的原因,如 401 认证失败);**灰** = 地址或令牌未配置
|
|
74
|
+
- 点 **测试连接** 会用**当前输入框里的内容**(未保存也可)立即测试,方便改完再存
|
|
75
|
+
|
|
67
76
|
以下环境变量 / 凭据仍作为**回退**(设置页未配置时生效,兼容旧部署),热加载无需重启:
|
|
68
77
|
|
|
69
78
|
```yaml
|
package/lib/client.js
CHANGED
|
@@ -47,7 +47,12 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
+ ".jt-set-card{box-sizing:border-box;list-style:none;margin:0;border:1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.28));background:var(--dsw-alias-bg-layer-1, rgba(127,127,127,.05));border-radius:10px;padding:10px 12px}"
|
|
48
48
|
+ ".jt-set-head{display:flex;flex-direction:column;gap:2px;margin-bottom:8px}"
|
|
49
49
|
+ ".jt-set-title{font-weight:600;font-size:13px;color:var(--dsw-alias-label-primary, #222)}"
|
|
50
|
-
+ ".jt-set-desc{font-size:12px;color:var(--dsw-alias-label-secondary, #666);line-height:1.6}"
|
|
50
|
+
+ ".jt-set-desc{font-size:12px;color:var(--dsw-alias-label-secondary, #666);line-height:1.6}"
|
|
51
|
+
+ ".jt-probe{display:flex;align-items:center;gap:6px;margin:0 0 8px;font-size:12px;color:var(--dsw-alias-label-secondary, #666);line-height:1.6;word-break:break-word}"
|
|
52
|
+
+ ".jt-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-label-secondary, #999)}"
|
|
53
|
+
+ ".jt-dot-ok{background:var(--dsw-alias-state-success-primary, #1a9d4f)}"
|
|
54
|
+
+ ".jt-dot-fail{background:var(--dsw-alias-state-error-primary, #d93026)}"
|
|
55
|
+
+ ".jt-dot-testing{background:var(--dsw-alias-state-warn-primary, #b76e00)}";
|
|
51
56
|
|
|
52
57
|
var STORAGE_KEY = "dsh.jiraTasks.config.v1";
|
|
53
58
|
|
|
@@ -107,6 +112,15 @@ window.__ModuleLoader__.load({
|
|
|
107
112
|
}).then(function (r) { return r.json(); });
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
// 探测 JIRA 地址/令牌可用性;可选传入未保存的草稿值。
|
|
116
|
+
function testConnection(payload) {
|
|
117
|
+
return fetch("/jira/api/test", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "Content-Type": "application/json" },
|
|
120
|
+
body: JSON.stringify(payload || {})
|
|
121
|
+
}).then(function (r) { return r.json(); });
|
|
122
|
+
}
|
|
123
|
+
|
|
110
124
|
function JiraTasksDock(props) {
|
|
111
125
|
var blankOnly = !!(props && props.blankOnly === true);
|
|
112
126
|
var useWorkspaces = props && typeof props.useWorkspaces === "function" ? props.useWorkspaces : null;
|
|
@@ -313,6 +327,9 @@ window.__ModuleLoader__.load({
|
|
|
313
327
|
var busy = busyState[0], setBusy = busyState[1];
|
|
314
328
|
var failedState = React.useState("");
|
|
315
329
|
var failed = failedState[0], setFailed = failedState[1];
|
|
330
|
+
var probeState = React.useState({ phase: "idle" });
|
|
331
|
+
var probe = probeState[0], setProbe = probeState[1];
|
|
332
|
+
var probeGen = React.useRef(0);
|
|
316
333
|
|
|
317
334
|
React.useEffect(function () {
|
|
318
335
|
var alive = true;
|
|
@@ -320,6 +337,12 @@ window.__ModuleLoader__.load({
|
|
|
320
337
|
return function () { alive = false; };
|
|
321
338
|
}, []);
|
|
322
339
|
|
|
340
|
+
// 自动测试:命名空间就绪后、以及已保存配置变更(含保存后)时各探测一次。
|
|
341
|
+
React.useEffect(function () {
|
|
342
|
+
if (snapshot.status !== "ready") return;
|
|
343
|
+
runProbe({});
|
|
344
|
+
}, [snapshot.status, snapshot.revision]);
|
|
345
|
+
|
|
323
346
|
var storedBase = snapshot.value && typeof snapshot.value.baseUrl === "string" ? snapshot.value.baseUrl : "";
|
|
324
347
|
var baseValue = baseEdit === undefined ? storedBase : baseEdit;
|
|
325
348
|
var dirty = (baseEdit !== undefined && baseEdit !== storedBase) || (tokenDraft || "").length > 0;
|
|
@@ -364,9 +387,27 @@ window.__ModuleLoader__.load({
|
|
|
364
387
|
setFailed("");
|
|
365
388
|
}
|
|
366
389
|
|
|
390
|
+
// 探测连接:payload 传入即测草稿,传空对象则测已保存/环境配置。
|
|
391
|
+
function runProbe(payload) {
|
|
392
|
+
var gen = ++probeGen.current;
|
|
393
|
+
setProbe({ phase: "testing", message: "正在测试连接…" });
|
|
394
|
+
testConnection(payload).then(function (res) {
|
|
395
|
+
if (gen !== probeGen.current) return;
|
|
396
|
+
if (res && res.ok) setProbe({ phase: "ok", message: "可用" + (res.user ? ":" + res.user : "") });
|
|
397
|
+
else if (res && res.code === "unconfigured") setProbe({ phase: "idle", message: "未配置地址或令牌" });
|
|
398
|
+
else setProbe({ phase: "fail", message: (res && res.error) || "不可用" });
|
|
399
|
+
}).catch(function (err) {
|
|
400
|
+
if (gen !== probeGen.current) return;
|
|
401
|
+
setProbe({ phase: "fail", message: String((err && err.message) || err) });
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
367
405
|
var tokenHint = tokenInfo === null
|
|
368
406
|
? "正在读取令牌状态…"
|
|
369
407
|
: (tokenInfo.configured ? "令牌已配置;留空并保存可保持不变。" : "令牌未配置。含 “:” 时用 Basic,否则用 Bearer。");
|
|
408
|
+
var dotClass = probe.phase === "ok" ? "jt-dot jt-dot-ok"
|
|
409
|
+
: probe.phase === "fail" ? "jt-dot jt-dot-fail"
|
|
410
|
+
: probe.phase === "testing" ? "jt-dot jt-dot-testing" : "jt-dot";
|
|
370
411
|
|
|
371
412
|
return h("li", { className: "jt-set-card" },
|
|
372
413
|
h("div", { className: "jt-set-head" },
|
|
@@ -400,8 +441,19 @@ window.__ModuleLoader__.load({
|
|
|
400
441
|
})
|
|
401
442
|
),
|
|
402
443
|
h("div", { className: "jt-hint", style: { marginBottom: "8px" } }, tokenHint),
|
|
444
|
+
h("div", { className: "jt-probe", role: "status" },
|
|
445
|
+
h("span", { className: dotClass, "aria-hidden": "true" }),
|
|
446
|
+
h("span", null, probe.message || "未测试")
|
|
447
|
+
),
|
|
403
448
|
failed ? h("div", { className: "jt-error", style: { marginBottom: "8px" } }, "保存失败:" + failed) : null,
|
|
404
449
|
h("div", { className: "jt-buttons" },
|
|
450
|
+
h("button", {
|
|
451
|
+
className: "jt-btn",
|
|
452
|
+
style: { marginRight: "auto" },
|
|
453
|
+
disabled: busy || probe.phase === "testing",
|
|
454
|
+
title: "用当前填写的内容测试 JIRA 地址与令牌",
|
|
455
|
+
onClick: function () { runProbe({ baseUrl: baseValue, token: (tokenDraft || "").trim() }); }
|
|
456
|
+
}, probe.phase === "testing" ? "测试中…" : "测试连接"),
|
|
405
457
|
h("button", { className: "jt-btn", disabled: !dirty || busy, onClick: discard }, "放弃"),
|
|
406
458
|
h("button", { className: "jt-btn jt-btn-primary", disabled: !dirty || busy || !writable, onClick: save }, busy ? "保存中…" : "保存")
|
|
407
459
|
)
|
package/lib/index.js
CHANGED
|
@@ -117,6 +117,49 @@ export default {
|
|
|
117
117
|
return { exitCode: outcome.exitCode, stdout, stderr };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// 从 JIRA 错误响应体/curl stderr 中提取可读信息。
|
|
121
|
+
function errorTextFrom(body, stderr, fallback) {
|
|
122
|
+
let message = (stderr || '').trim() || fallback;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(body);
|
|
125
|
+
if (parsed.errorMessages && parsed.errorMessages.length) message = parsed.errorMessages.join(';');
|
|
126
|
+
else if (parsed.message) message = parsed.message;
|
|
127
|
+
} catch (e) { /* body 非 JSON */ }
|
|
128
|
+
return message;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 连通性/鉴权探测:GET /rest/api/2/myself,stdout 末尾追加 HTTP 状态码。
|
|
132
|
+
async function probeJira(baseUrl, token) {
|
|
133
|
+
const auth = buildAuthHeader(token);
|
|
134
|
+
const cleanBase = baseUrl.replace(/\/+$/, '');
|
|
135
|
+
let curl;
|
|
136
|
+
try {
|
|
137
|
+
curl = await subprocess.resolveExecutable('curl');
|
|
138
|
+
} catch (e) {
|
|
139
|
+
curl = '/usr/bin/curl';
|
|
140
|
+
}
|
|
141
|
+
const handle = subprocess.spawn({
|
|
142
|
+
argv: [
|
|
143
|
+
curl, '-sS', '--max-time', '15',
|
|
144
|
+
'-H', 'Accept: application/json',
|
|
145
|
+
'--config', '-',
|
|
146
|
+
'-w', '\n%{http_code}',
|
|
147
|
+
cleanBase + '/rest/api/2/myself'
|
|
148
|
+
],
|
|
149
|
+
cwd: workspaceRoot,
|
|
150
|
+
stdio: {
|
|
151
|
+
stdin: { data: 'header = "Authorization: ' + auth + '"\n' },
|
|
152
|
+
stdout: { maxBytes: 1024 * 1024, spill: { maxBytes: 4 * 1024 * 1024 } },
|
|
153
|
+
stderr: { maxBytes: 256 * 1024, spill: { maxBytes: 1024 * 1024 } }
|
|
154
|
+
},
|
|
155
|
+
graceMs: 5000
|
|
156
|
+
});
|
|
157
|
+
const outcome = await handle.done;
|
|
158
|
+
const stdout = (handle.collected.stdout ? handle.collected.stdout.readFrom(0).text : '') || '';
|
|
159
|
+
const stderr = (handle.collected.stderr ? handle.collected.stderr.readFrom(0).text : '') || '';
|
|
160
|
+
return { exitCode: outcome.exitCode, stdout, stderr };
|
|
161
|
+
}
|
|
162
|
+
|
|
120
163
|
async function handleSearch(args) {
|
|
121
164
|
const projectKey = String((args && args.projectKey) || '').trim();
|
|
122
165
|
if (!projectKey) return { ok: false, error: '未设置项目 Key' };
|
|
@@ -132,13 +175,7 @@ export default {
|
|
|
132
175
|
const stderr = raw.stderr;
|
|
133
176
|
|
|
134
177
|
if (raw.exitCode !== 0) {
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const parsed = JSON.parse(stdout);
|
|
138
|
-
if (parsed.errorMessages && parsed.errorMessages.length) message = parsed.errorMessages.join(';');
|
|
139
|
-
else if (parsed.message) message = parsed.message;
|
|
140
|
-
} catch (e) { /* stdout 非 JSON */ }
|
|
141
|
-
return { ok: false, error: message };
|
|
178
|
+
return { ok: false, error: errorTextFrom(stdout, stderr, 'curl 退出码 ' + String(raw.exitCode)) };
|
|
142
179
|
}
|
|
143
180
|
|
|
144
181
|
let data;
|
|
@@ -169,6 +206,43 @@ export default {
|
|
|
169
206
|
}
|
|
170
207
|
}
|
|
171
208
|
|
|
209
|
+
// 设置卡片“测试连接”:用草稿值(未保存也可测)或已保存/环境配置探测 JIRA。
|
|
210
|
+
async function handleTest(args) {
|
|
211
|
+
try {
|
|
212
|
+
const draftBase = args && typeof args.baseUrl === 'string' ? args.baseUrl.trim() : '';
|
|
213
|
+
const draftToken = args && typeof args.token === 'string' ? args.token.trim() : '';
|
|
214
|
+
const baseUrl = draftBase || settingsBaseUrl() || await resolveFirst(['JIRA_BASE_URL', 'JIRA_URL']);
|
|
215
|
+
if (!baseUrl) return { ok: false, code: 'unconfigured', error: '未配置 JIRA 地址' };
|
|
216
|
+
const token = draftToken || await resolveFirst(['JIRA_API_TOKEN', 'JIRA_TOKEN']);
|
|
217
|
+
if (!token) return { ok: false, code: 'unconfigured', error: '未配置 JIRA 令牌' };
|
|
218
|
+
|
|
219
|
+
const raw = await probeJira(baseUrl, token);
|
|
220
|
+
if (raw.exitCode !== 0) {
|
|
221
|
+
return { ok: false, code: 'network', error: (raw.stderr || '').trim() || ('curl 退出码 ' + String(raw.exitCode)) };
|
|
222
|
+
}
|
|
223
|
+
const nl = raw.stdout.lastIndexOf('\n');
|
|
224
|
+
const body = nl === -1 ? raw.stdout : raw.stdout.slice(0, nl);
|
|
225
|
+
const httpCode = Number((nl === -1 ? '' : raw.stdout.slice(nl + 1)).trim()) || 0;
|
|
226
|
+
|
|
227
|
+
if (httpCode >= 200 && httpCode < 300) {
|
|
228
|
+
let data = {};
|
|
229
|
+
try { data = JSON.parse(body); } catch (e) { /* 空/非 JSON 也视为连通 */ }
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
baseUrl: baseUrl.replace(/\/+$/, ''),
|
|
233
|
+
user: data.displayName || data.name || data.key || ''
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
code: httpCode === 401 || httpCode === 403 ? 'auth' : 'http',
|
|
239
|
+
error: errorTextFrom(body, raw.stderr, 'HTTP ' + String(httpCode))
|
|
240
|
+
};
|
|
241
|
+
} catch (err) {
|
|
242
|
+
return { ok: false, code: 'error', error: String((err && err.message) || err) };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
172
246
|
if (webServer && typeof webServer.register === 'function') {
|
|
173
247
|
webServer.register({
|
|
174
248
|
kind: 'exact',
|
|
@@ -181,6 +255,17 @@ export default {
|
|
|
181
255
|
}
|
|
182
256
|
}
|
|
183
257
|
});
|
|
258
|
+
webServer.register({
|
|
259
|
+
kind: 'exact',
|
|
260
|
+
path: '/jira/api/test',
|
|
261
|
+
handler: async (req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
sendJson(res, await handleTest(await readBody(req)));
|
|
264
|
+
} catch (e) {
|
|
265
|
+
sendJson(res, { ok: false, code: 'error', error: String((e && e.message) || e) });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
184
269
|
}
|
|
185
270
|
}
|
|
186
271
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-jira-tasks",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "JIRA open tasks panel for DeepSeek Harness (DSH): shows the current user's open/reopened issues below the composer, per-workspace project key, persistent profile bundle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|