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 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 repo's `profile-package/` to `~/.dsh/profiles/web/packages/dsh-jira-tasks/`
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. 将仓库 `profile-package/` 复制为 `~/.dsh/profiles/web/packages/dsh-jira-tasks/`
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: false };
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 info = res.value[TOKEN_REFS[i]];
295
- if (info && info.configured) return { configured: true, writable: info.writable !== false };
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
- return { configured: false, writable: false };
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
- setFailed(String((err && err.message) || err));
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
- : (tokenInfo.configured ? "令牌已配置;留空并保存可保持不变。" : "令牌未配置。含 “:” 时用 Basic,否则用 Bearer。");
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
- let message = stderr.trim() || ('curl 退出码 ' + String(raw.exitCode));
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.5",
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",