dsh-jira-tasks 1.0.5 → 1.0.7
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 +32 -1
- package/README.md +35 -1
- package/lib/client.js +91 -11
- 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
|
|
@@ -24,12 +25,18 @@ Published to npm:
|
|
|
24
25
|
dsh plugin --profile web add dsh-jira-tasks
|
|
25
26
|
```
|
|
26
27
|
|
|
28
|
+
Or straight from GitHub (the repository root is the package directory; `lib/` is prebuilt):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
dsh plugin --profile web add github:liu3734/jira-tasks-dsh-plugin
|
|
32
|
+
```
|
|
33
|
+
|
|
27
34
|
**Restart DSH** to activate.
|
|
28
35
|
|
|
29
36
|
<details>
|
|
30
37
|
<summary>Manual install (without npm)</summary>
|
|
31
38
|
|
|
32
|
-
1. Copy the
|
|
39
|
+
1. Copy this repository (its root is the package directory) to `~/.dsh/profiles/web/packages/dsh-jira-tasks/` (skip `.git/`)
|
|
33
40
|
2. Edit `~/.dsh/profiles/web/package.json`:
|
|
34
41
|
- Add to `dependencies`: `"dsh-jira-tasks": "file:./packages/dsh-jira-tasks"`
|
|
35
42
|
- Append to `dsh.profile.bundles`: `"dsh-jira-tasks"`
|
|
@@ -56,6 +63,16 @@ Open **Settings → Plugins → Plugin configuration → JIRA** and fill in:
|
|
|
56
63
|
|
|
57
64
|
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
65
|
|
|
66
|
+
> **A token supplied by the environment cannot be overwritten from this card.** If the environment that launched DSH already defines `JIRA_API_TOKEN` (a Windows *user-level* variable counts), DSH treats that reference as read-only: the card disables the token field and explains why. The panel already uses that variable, so **no save is needed**; to manage the token from settings instead, remove the variable first (Windows: System Properties → Environment Variables, or PowerShell `[Environment]::SetEnvironmentVariable('JIRA_API_TOKEN', $null, 'User')`) and restart DSH.
|
|
67
|
+
|
|
68
|
+
#### Connection test
|
|
69
|
+
|
|
70
|
+
The card's footer carries a status light and a **Test connection** button:
|
|
71
|
+
|
|
72
|
+
- Opening the card **auto-probes** once (against JIRA `/rest/api/2/myself`), and saving re-probes
|
|
73
|
+
- **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
|
|
74
|
+
- **Test connection** probes what is currently in the fields, saved or not, so you can check before saving
|
|
75
|
+
|
|
59
76
|
Environment variables / credentials still work as a **fallback** (used when the settings card is empty), hot-reloaded without a restart:
|
|
60
77
|
|
|
61
78
|
```yaml
|
|
@@ -99,6 +116,20 @@ dsh plugin --profile web remove dsh-jira-tasks
|
|
|
99
116
|
| Cannot parse JIRA response | Network / proxy issue, curl produced no output |
|
|
100
117
|
</details>
|
|
101
118
|
|
|
119
|
+
<details>
|
|
120
|
+
<summary>Saving the token fails with "is supplied read-only by the launching environment"</summary>
|
|
121
|
+
|
|
122
|
+
`JIRA_API_TOKEN` (or `JIRA_TOKEN`) is supplied by the environment that launched DSH, and DSH treats that reference as read-only: a write would be shadowed by the variable, so it is refused. The panel already uses that variable and queries fine, so **there is nothing to save**.
|
|
123
|
+
|
|
124
|
+
To manage the token from the settings card instead, remove the variable and restart DSH:
|
|
125
|
+
|
|
126
|
+
- Windows (PowerShell): `[Environment]::SetEnvironmentVariable('JIRA_API_TOKEN', $null, 'User')`, then reopen the terminal
|
|
127
|
+
- Windows (GUI): System Properties → Advanced → Environment Variables, delete the user variable
|
|
128
|
+
- macOS / Linux: remove the export from `~/.zshrc` / `~/.bashrc` (or equivalent) and reopen the terminal
|
|
129
|
+
|
|
130
|
+
Since v1.0.7 the card detects a read-only token, disables the field, and shows this guidance inline instead of failing on save.
|
|
131
|
+
</details>
|
|
132
|
+
|
|
102
133
|
<details>
|
|
103
134
|
<summary>Panel does not appear</summary>
|
|
104
135
|
|
package/README.md
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
- 👤 默认仅显示当前用户(`assignee = currentUser()`)的「开启 / 重新开启」任务
|
|
17
17
|
- ⚙️ **设置 → 插件 → JIRA** 配置 JIRA 地址与访问令牌(令牌写入凭据存储,不回传前端)
|
|
18
|
+
- 🟢 设置卡片自动探测连接并显示状态灯:绿=可用、红=不可用、灰=未配置;点「测试连接」可用**未保存的草稿值**即时验证
|
|
19
|
+
|
|
20
|
+
<img width="1590" height="1574" alt="image" src="https://github.com/user-attachments/assets/af5f11c6-aa9a-4c3b-84ca-cff6c0017b22" />
|
|
21
|
+
|
|
18
22
|
- ⚙️ 项目 Key 与 JQL 按工作区保存;未配置的工作区显示"未配置"
|
|
19
23
|
- 🔄 打开新会话自动查询,面板内支持一键刷新(⟳)
|
|
20
24
|
- 🧭 点击标题可**收起 / 展开**面板;标题徽标显示查询命中的任务总数(列表最多展示 50 条,按更新时间倒序)
|
|
@@ -30,6 +34,12 @@
|
|
|
30
34
|
dsh plugin --profile web add dsh-jira-tasks
|
|
31
35
|
```
|
|
32
36
|
|
|
37
|
+
或直接从 GitHub 安装(仓库根即包目录,`lib/` 为预构建产物):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
dsh plugin --profile web add github:liu3734/jira-tasks-dsh-plugin
|
|
41
|
+
```
|
|
42
|
+
|
|
33
43
|
**重启 DSH** 后生效。
|
|
34
44
|
|
|
35
45
|
> 若改用 GitHub Packages 源:先在 profile 的 `.npmrc` 配置 `@liu3734:registry=https://npm.pkg.github.com/` 及读取令牌,再执行 `dsh plugin --profile web add @liu3734/dsh-jira-tasks`。
|
|
@@ -37,7 +47,7 @@ dsh plugin --profile web add dsh-jira-tasks
|
|
|
37
47
|
<details>
|
|
38
48
|
<summary>手动安装(不使用 npm)</summary>
|
|
39
49
|
|
|
40
|
-
1.
|
|
50
|
+
1. 将本仓库(仓库根目录即包目录)复制为 `~/.dsh/profiles/web/packages/dsh-jira-tasks/`(可忽略 `.git/`)
|
|
41
51
|
2. 编辑 `~/.dsh/profiles/web/package.json`:
|
|
42
52
|
- `dependencies` 增加:`"dsh-jira-tasks": "file:./packages/dsh-jira-tasks"`
|
|
43
53
|
- `dsh.profile.bundles` 追加:`"dsh-jira-tasks"`
|
|
@@ -64,6 +74,16 @@ dsh plugin --profile web add dsh-jira-tasks
|
|
|
64
74
|
|
|
65
75
|
留空并保存会保持已有令牌不变;地址留空并保存则清除设置项,回退到环境变量。认证自动识别:令牌含 `:` 用 Basic,否则用 Bearer(JIRA PAT)。
|
|
66
76
|
|
|
77
|
+
> **令牌已由环境变量提供时,设置页无法改写它**:若启动 DSH 的环境里已设置 `JIRA_API_TOKEN`(Windows 用户级环境变量也算),DSH 会将该引用视为只读,卡片会禁用令牌输入并说明原因。此时面板已直接使用该环境变量,**无需保存**;要改由设置页管理,请先移除环境变量(Windows:系统属性 → 环境变量,或 PowerShell `[Environment]::SetEnvironmentVariable('JIRA_API_TOKEN', $null, 'User')`)后重启 DSH。
|
|
78
|
+
|
|
79
|
+
#### 连接测试
|
|
80
|
+
|
|
81
|
+
卡片底部有连接状态灯与 **测试连接** 按钮:
|
|
82
|
+
|
|
83
|
+
- 打开卡片时会**自动探测**一次(请求 JIRA `/rest/api/2/myself`),保存后也会自动重测
|
|
84
|
+
- **绿** = 地址与令牌可用(并显示当前登录用户);**红** = 不可用(显示 JIRA 返回的原因,如 401 认证失败);**灰** = 地址或令牌未配置
|
|
85
|
+
- 点 **测试连接** 会用**当前输入框里的内容**(未保存也可)立即测试,方便改完再存
|
|
86
|
+
|
|
67
87
|
以下环境变量 / 凭据仍作为**回退**(设置页未配置时生效,兼容旧部署),热加载无需重启:
|
|
68
88
|
|
|
69
89
|
```yaml
|
|
@@ -110,6 +130,20 @@ dsh plugin --profile web remove dsh-jira-tasks
|
|
|
110
130
|
| 无法解析 JIRA 响应:… | 网络 / 代理问题,curl 无输出 |
|
|
111
131
|
</details>
|
|
112
132
|
|
|
133
|
+
<details>
|
|
134
|
+
<summary>保存令牌报 “is supplied read-only by the launching environment”</summary>
|
|
135
|
+
|
|
136
|
+
`JIRA_API_TOKEN`(或 `JIRA_TOKEN`)已由启动 DSH 的环境提供,DSH 判定该引用只读:写入会被环境变量遮蔽,因此拒绝保存。此时面板已经直接使用该环境变量,能正常查询,**无需在设置页保存**。
|
|
137
|
+
|
|
138
|
+
如需改由设置页管理令牌,先移除环境变量再重启 DSH:
|
|
139
|
+
|
|
140
|
+
- Windows(PowerShell):`[Environment]::SetEnvironmentVariable('JIRA_API_TOKEN', $null, 'User')`,然后重开终端
|
|
141
|
+
- Windows(图形界面):系统属性 → 高级 → 环境变量,删除对应用户变量
|
|
142
|
+
- macOS / Linux:从 `~/.zshrc` / `~/.bashrc` 等启动脚本中移除后重开终端
|
|
143
|
+
|
|
144
|
+
v1.0.7 起卡片会检测只读令牌并直接禁用输入框、在卡片内给出上述提示,不再等到保存才报英文错误。
|
|
145
|
+
</details>
|
|
146
|
+
|
|
113
147
|
<details>
|
|
114
148
|
<summary>面板不显示</summary>
|
|
115
149
|
|
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;
|
|
@@ -274,6 +288,18 @@ window.__ModuleLoader__.load({
|
|
|
274
288
|
var TOKEN_REF = "JIRA_API_TOKEN";
|
|
275
289
|
var TOKEN_REFS = ["JIRA_API_TOKEN", "JIRA_TOKEN"];
|
|
276
290
|
|
|
291
|
+
// credentials-local 拒绝写入“由启动环境提供”的引用时,原始信息是英文的;这里取出引用名并给出可操作的中文说明。
|
|
292
|
+
function shadowedTokenRef(message) {
|
|
293
|
+
var m = /"([^"]+)" is supplied read-only by the launching environment/.exec(String(message || ""));
|
|
294
|
+
return m ? m[1] : "";
|
|
295
|
+
}
|
|
296
|
+
function tokenReadOnlyHint(ref) {
|
|
297
|
+
var name = ref || TOKEN_REF;
|
|
298
|
+
return "令牌由启动 DSH 的环境变量 " + name + " 提供(只读),面板已直接使用,无需在此保存。"
|
|
299
|
+
+ "如需改由设置页管理,请先移除该环境变量并重启 DSH;Windows 可在 PowerShell 执行 "
|
|
300
|
+
+ "[Environment]::SetEnvironmentVariable('" + name + "', $null, 'User') 后重开终端。";
|
|
301
|
+
}
|
|
302
|
+
|
|
277
303
|
function useScopeSnapshot(scope) {
|
|
278
304
|
var state = React.useState(function () { return scope.getSnapshot(); });
|
|
279
305
|
var snapshot = state[0], setSnapshot = state[1];
|
|
@@ -289,15 +315,20 @@ window.__ModuleLoader__.load({
|
|
|
289
315
|
function createJiraSettingsCard(scope, credentials) {
|
|
290
316
|
function readTokenInfo() {
|
|
291
317
|
return credentials.describe(TOKEN_REFS).then(function (res) {
|
|
292
|
-
if (!res || !res.ok || !res.value) return { configured: false, writable:
|
|
318
|
+
if (!res || !res.ok || !res.value) return { configured: false, writable: true };
|
|
293
319
|
for (var i = 0; i < TOKEN_REFS.length; i++) {
|
|
294
|
-
var
|
|
295
|
-
|
|
320
|
+
var ref = TOKEN_REFS[i];
|
|
321
|
+
var info = res.value[ref];
|
|
322
|
+
// 命中的第一个引用即生效来源;writable 为 false 表示只读(由启动环境提供),写不进去。
|
|
323
|
+
if (info && info.configured) {
|
|
324
|
+
return { configured: true, writable: info.writable !== false, ref: ref };
|
|
325
|
+
}
|
|
296
326
|
}
|
|
297
327
|
var primary = res.value[TOKEN_REF];
|
|
298
|
-
return { configured: false, writable: primary ? primary.writable !== false : true };
|
|
328
|
+
return { configured: false, writable: primary ? primary.writable !== false : true, ref: TOKEN_REF };
|
|
299
329
|
}).catch(function () {
|
|
300
|
-
|
|
330
|
+
// 状态读取失败不阻断保存,交由写入路径给出真实错误。
|
|
331
|
+
return { configured: false, writable: true };
|
|
301
332
|
});
|
|
302
333
|
}
|
|
303
334
|
|
|
@@ -313,6 +344,9 @@ window.__ModuleLoader__.load({
|
|
|
313
344
|
var busy = busyState[0], setBusy = busyState[1];
|
|
314
345
|
var failedState = React.useState("");
|
|
315
346
|
var failed = failedState[0], setFailed = failedState[1];
|
|
347
|
+
var probeState = React.useState({ phase: "idle" });
|
|
348
|
+
var probe = probeState[0], setProbe = probeState[1];
|
|
349
|
+
var probeGen = React.useRef(0);
|
|
316
350
|
|
|
317
351
|
React.useEffect(function () {
|
|
318
352
|
var alive = true;
|
|
@@ -320,10 +354,19 @@ window.__ModuleLoader__.load({
|
|
|
320
354
|
return function () { alive = false; };
|
|
321
355
|
}, []);
|
|
322
356
|
|
|
357
|
+
// 自动测试:命名空间就绪后、以及已保存配置变更(含保存后)时各探测一次。
|
|
358
|
+
React.useEffect(function () {
|
|
359
|
+
if (snapshot.status !== "ready") return;
|
|
360
|
+
runProbe({});
|
|
361
|
+
}, [snapshot.status, snapshot.revision]);
|
|
362
|
+
|
|
323
363
|
var storedBase = snapshot.value && typeof snapshot.value.baseUrl === "string" ? snapshot.value.baseUrl : "";
|
|
324
364
|
var baseValue = baseEdit === undefined ? storedBase : baseEdit;
|
|
325
365
|
var dirty = (baseEdit !== undefined && baseEdit !== storedBase) || (tokenDraft || "").length > 0;
|
|
326
366
|
var writable = snapshot.writable !== false;
|
|
367
|
+
// 令牌可能来自启动环境(只读):此时禁用输入并说明原因,而不是等到保存才报 DSH 的英文错误。
|
|
368
|
+
var tokenWritable = !tokenInfo || tokenInfo.writable !== false;
|
|
369
|
+
var tokenReadOnly = !!(tokenInfo && tokenInfo.configured && tokenInfo.writable === false);
|
|
327
370
|
|
|
328
371
|
// 命名空间未挂载时不显示,避免留下一张无法操作的卡片。
|
|
329
372
|
if (snapshot.status === "unavailable") return null;
|
|
@@ -337,6 +380,11 @@ window.__ModuleLoader__.load({
|
|
|
337
380
|
tasks.push(baseEdit === "" ? scope.unset("baseUrl") : scope.set("baseUrl", baseEdit));
|
|
338
381
|
}
|
|
339
382
|
var token = (tokenDraft || "").trim();
|
|
383
|
+
if (token && !tokenWritable) {
|
|
384
|
+
setBusy(false);
|
|
385
|
+
setFailed(tokenReadOnlyHint(tokenInfo && tokenInfo.ref));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
340
388
|
if (token) {
|
|
341
389
|
tasks.push(credentials.set(TOKEN_REF, token).then(function (res) {
|
|
342
390
|
if (res && res.ok === false) {
|
|
@@ -354,7 +402,8 @@ window.__ModuleLoader__.load({
|
|
|
354
402
|
setBusy(false);
|
|
355
403
|
}).catch(function (err) {
|
|
356
404
|
setBusy(false);
|
|
357
|
-
|
|
405
|
+
var shadowed = shadowedTokenRef(err && err.message);
|
|
406
|
+
setFailed(shadowed ? tokenReadOnlyHint(shadowed) : String((err && err.message) || err));
|
|
358
407
|
});
|
|
359
408
|
}
|
|
360
409
|
|
|
@@ -364,9 +413,29 @@ window.__ModuleLoader__.load({
|
|
|
364
413
|
setFailed("");
|
|
365
414
|
}
|
|
366
415
|
|
|
416
|
+
// 探测连接:payload 传入即测草稿,传空对象则测已保存/环境配置。
|
|
417
|
+
function runProbe(payload) {
|
|
418
|
+
var gen = ++probeGen.current;
|
|
419
|
+
setProbe({ phase: "testing", message: "正在测试连接…" });
|
|
420
|
+
testConnection(payload).then(function (res) {
|
|
421
|
+
if (gen !== probeGen.current) return;
|
|
422
|
+
if (res && res.ok) setProbe({ phase: "ok", message: "可用" + (res.user ? ":" + res.user : "") });
|
|
423
|
+
else if (res && res.code === "unconfigured") setProbe({ phase: "idle", message: "未配置地址或令牌" });
|
|
424
|
+
else setProbe({ phase: "fail", message: (res && res.error) || "不可用" });
|
|
425
|
+
}).catch(function (err) {
|
|
426
|
+
if (gen !== probeGen.current) return;
|
|
427
|
+
setProbe({ phase: "fail", message: String((err && err.message) || err) });
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
367
431
|
var tokenHint = tokenInfo === null
|
|
368
432
|
? "正在读取令牌状态…"
|
|
369
|
-
: (
|
|
433
|
+
: (tokenReadOnly
|
|
434
|
+
? tokenReadOnlyHint(tokenInfo.ref)
|
|
435
|
+
: (tokenInfo.configured ? "令牌已配置;留空并保存可保持不变。" : "令牌未配置。含 “:” 时用 Basic,否则用 Bearer。"));
|
|
436
|
+
var dotClass = probe.phase === "ok" ? "jt-dot jt-dot-ok"
|
|
437
|
+
: probe.phase === "fail" ? "jt-dot jt-dot-fail"
|
|
438
|
+
: probe.phase === "testing" ? "jt-dot jt-dot-testing" : "jt-dot";
|
|
370
439
|
|
|
371
440
|
return h("li", { className: "jt-set-card" },
|
|
372
441
|
h("div", { className: "jt-set-head" },
|
|
@@ -392,18 +461,29 @@ window.__ModuleLoader__.load({
|
|
|
392
461
|
className: "jt-input",
|
|
393
462
|
type: "password",
|
|
394
463
|
value: tokenDraft,
|
|
395
|
-
placeholder: tokenInfo && tokenInfo.configured ? "已配置,留空保持不变" : "粘贴令牌",
|
|
464
|
+
placeholder: tokenReadOnly ? "由环境变量提供(只读)" : (tokenInfo && tokenInfo.configured ? "已配置,留空保持不变" : "粘贴令牌"),
|
|
396
465
|
autoComplete: "off",
|
|
397
|
-
disabled: !writable || busy,
|
|
466
|
+
disabled: !writable || busy || !tokenWritable,
|
|
398
467
|
onChange: function (e) { setTokenDraft(e.target.value); },
|
|
399
468
|
onKeyDown: function (e) { if (e.key === "Enter") save(); }
|
|
400
469
|
})
|
|
401
470
|
),
|
|
402
471
|
h("div", { className: "jt-hint", style: { marginBottom: "8px" } }, tokenHint),
|
|
472
|
+
h("div", { className: "jt-probe", role: "status" },
|
|
473
|
+
h("span", { className: dotClass, "aria-hidden": "true" }),
|
|
474
|
+
h("span", null, probe.message || "未测试")
|
|
475
|
+
),
|
|
403
476
|
failed ? h("div", { className: "jt-error", style: { marginBottom: "8px" } }, "保存失败:" + failed) : null,
|
|
404
477
|
h("div", { className: "jt-buttons" },
|
|
478
|
+
h("button", {
|
|
479
|
+
className: "jt-btn",
|
|
480
|
+
style: { marginRight: "auto" },
|
|
481
|
+
disabled: busy || probe.phase === "testing",
|
|
482
|
+
title: "用当前填写的内容测试 JIRA 地址与令牌",
|
|
483
|
+
onClick: function () { runProbe({ baseUrl: baseValue, token: (tokenDraft || "").trim() }); }
|
|
484
|
+
}, probe.phase === "testing" ? "测试中…" : "测试连接"),
|
|
405
485
|
h("button", { className: "jt-btn", disabled: !dirty || busy, onClick: discard }, "放弃"),
|
|
406
|
-
h("button", { className: "jt-btn jt-btn-primary", disabled: !dirty || busy || !writable, onClick: save }, busy ? "保存中…" : "保存")
|
|
486
|
+
h("button", { className: "jt-btn jt-btn-primary", disabled: !dirty || busy || !writable || ((tokenDraft || "").trim().length > 0 && !tokenWritable), onClick: save }, busy ? "保存中…" : "保存")
|
|
407
487
|
)
|
|
408
488
|
);
|
|
409
489
|
};
|
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.7",
|
|
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",
|