dsh-jira-tasks 1.0.4 → 1.0.5

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
@@ -4,12 +4,13 @@
4
4
 
5
5
  [中文](README.md) · **English**
6
6
 
7
- Shows the current JIRA project's **open / reopened** issues **assigned to the current user** below the DSH composer input. The JIRA base URL and token are read from credentials; the project key and JQL are **configured per workspace** and persisted.
7
+ Shows the current JIRA project's **open / reopened** issues **assigned to the current user** below the DSH composer input. The JIRA base URL and token are configured in **Settings → Plugins → JIRA** (`JIRA_BASE_URL` / `JIRA_API_TOKEN` act as fallback); the project key and JQL are **configured per workspace** and persisted.
8
8
 
9
9
  ## Features
10
10
 
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
+ - ⚙️ **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)
13
14
  - ⚙️ Project key and JQL are saved per workspace; unconfigured workspaces show "unconfigured"
14
15
  - 🔄 Auto-query on every new session, with a one-click refresh (⟳)
15
16
  - 🔗 Click an issue to open its JIRA detail in a new tab
@@ -48,7 +49,14 @@ In a DSH session, use the Cordis tools: `cordis_define` (`kind: new`, `idPrefix:
48
49
 
49
50
  ### 1. JIRA base URL and token
50
51
 
51
- Write to `$DSH_HOME/.credentials.yaml` (recommended, hot-reloaded), or export environment variables before launching DSH:
52
+ Open **Settings Plugins Plugin configuration JIRA** and fill in:
53
+
54
+ - **JIRA base URL**: e.g. `http://jira.example.com/` (stored in the user settings document and read back by the form)
55
+ - **Access token / PAT**: written to the credential store (`$DSH_HOME/.credentials.yaml`); the browser only ever sees "configured", never the token itself
56
+
57
+ 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
+ Environment variables / credentials still work as a **fallback** (used when the settings card is empty), hot-reloaded without a restart:
52
60
 
53
61
  ```yaml
54
62
  JIRA_BASE_URL: "http://jira.example.com/"
@@ -57,7 +65,6 @@ JIRA_API_TOKEN: "<PAT or user:token>"
57
65
 
58
66
  - Base URL aliases: `JIRA_BASE_URL` / `JIRA_URL`
59
67
  - Token aliases: `JIRA_API_TOKEN` / `JIRA_TOKEN`
60
- - Auth is auto-detected: tokens containing `:` use Basic, otherwise Bearer (JIRA PAT)
61
68
 
62
69
  ### 2. Project key and JQL (per workspace)
63
70
 
@@ -86,7 +93,8 @@ dsh plugin --profile web remove dsh-jira-tasks
86
93
 
87
94
  | Message | Fix |
88
95
  |---|---|
89
- | JIRA_BASE_URL not configured | Credentials missing — see "Configuration 1" above |
96
+ | JIRA base URL not configured | Address missing — see "Configuration 1" above |
97
+ | JIRA token not configured | Token missing — see "Configuration 1" above |
90
98
  | 401 … | Invalid token or wrong auth scheme; verify with `curl -H "Authorization: Bearer <token>" <base>/rest/api/2/myself` |
91
99
  | Cannot parse JIRA response | Network / proxy issue, curl produced no output |
92
100
  </details>
@@ -107,15 +115,16 @@ dsh plugin --profile web remove dsh-jira-tasks
107
115
  ┌─────────── Browser (Client) ───────────┐ ┌──────────── Host ──────────────┐
108
116
  │ conversation.composer.dock (active) │ │ webServer route /jira/api/search │
109
117
  │ conversation.input.dock (new, order:99) │ │ ↓ │
110
- │ ↓ on mount/refresh fetch POST │ │ credentials.resolve(JIRA_*)
111
- │ render: list / error / unconfigured │ │ subprocess.spawn(curl …)
112
- │ localStorage per-workspace config │ │ stdout JSON
118
+ │ ↓ on mount/refresh fetch POST │ │ settings.get("jira-tasks")
119
+ │ render: list / error / unconfigured │ │ credentials.resolve(JIRA_*)
120
+ │ localStorage per-workspace key/JQL │ │ subprocess.spawn(curl …)
121
+ │ settings.plugin.item (Settings card) │ │ ↓ stdout JSON │
113
122
  └────────────────────────────────────────────┘ │ parse issues → {ok,issues} │
114
123
  └────────────────────────────────┘
115
124
  ```
116
125
 
117
- - **Host**: registers a `webServer` route `POST /jira/api/search`; credentials resolved via the `credentials` service (env / `$DSH_HOME/.credentials.yaml`, hot-reloaded); queries run through `subprocess` spawning `curl` directly, with the auth header passed via stdin (`--config -`) so the token never appears in argv.
118
- - **Client**: a standard `window.__ModuleLoader__.load({ id, factory })` web bundle; registers `conversation.composer.dock` (active sessions) and `conversation.input.dock` (new sessions, flex `order: 99` below the input, aligned width).
126
+ - **Host**: registers the `jira-tasks` settings namespace (`baseUrl`, readable) and a `webServer` route `POST /jira/api/search`; the address comes from the settings document, the token from the `credentials` service (Settings card / env / `$DSH_HOME/.credentials.yaml`, hot-reloaded); queries run through `subprocess` spawning `curl` directly, with the auth header passed via stdin (`--config -`) so the token never appears in argv.
127
+ - **Client**: a standard `window.__ModuleLoader__.load({ id, factory })` web bundle; registers `conversation.composer.dock` (active sessions) and `conversation.input.dock` (new sessions, flex `order: 99` below the input, aligned width), plus `settings.plugin.item` (`key: "jira-tasks"`) for the Settings card — the address is written through `settingsScope` and the token through `remote.credentials`.
119
128
  - **Why not the `shell` service**: `shell` wraps commands with `sandbox-exec`, which is broken on some macOS versions (`sandbox_apply: Operation not permitted`); `subprocess` is the raw process seam without this issue.
120
129
  - **New-session display**: the DSH shell does not render `composer.dock` during the hero (blank session) phase, so the plugin also registers `input.dock` and de-duplicates by "session has messages".
121
130
 
@@ -126,7 +135,7 @@ dsh plugin --profile web remove dsh-jira-tasks
126
135
  | Persistence | Lost on restart | Survives restart |
127
136
  | Client→Host | `host.call` / `harness.handle` | `webServer` route + `fetch` |
128
137
  | Client bundle | Injected per session | `/plugins/dsh-jira-tasks/client.js` |
129
- | Config / credentials | Same `localStorage` key, same `.credentials.yaml` | Identical |
138
+ | Config / credentials | Env / `.credentials.yaml` only (no Settings card) | Settings card + same `.credentials.yaml` fallback |
130
139
  </details>
131
140
 
132
141
  ## License
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **中文** · [English](README.en.md)
6
6
 
7
- 在 DSH 会话**输入框下方**展示当前 JIRA 项目**指派给当前用户**的「开启 / 重新开启」任务列表。JIRA 地址与令牌从凭据读取;项目 Key 与 JQL **按工作区配置**并持久化。
7
+ 在 DSH 会话**输入框下方**展示当前 JIRA 项目**指派给当前用户**的「开启 / 重新开启」任务列表。JIRA 地址与令牌在**设置 → 插件 → JIRA** 中配置(`JIRA_BASE_URL` / `JIRA_API_TOKEN` 作为回退);项目 Key 与 JQL **按工作区配置**并持久化。
8
8
 
9
9
  ## 功能
10
10
 
@@ -14,14 +14,17 @@
14
14
  <img width="1972" height="746" alt="image" src="https://github.com/user-attachments/assets/9f543e04-a5ad-4b86-85c8-baa2de26f44e" />
15
15
 
16
16
  - 👤 默认仅显示当前用户(`assignee = currentUser()`)的「开启 / 重新开启」任务
17
+ - ⚙️ **设置 → 插件 → JIRA** 配置 JIRA 地址与访问令牌(令牌写入凭据存储,不回传前端)
17
18
  - ⚙️ 项目 Key 与 JQL 按工作区保存;未配置的工作区显示"未配置"
18
19
  - 🔄 打开新会话自动查询,面板内支持一键刷新(⟳)
20
+ - 🧭 点击标题可**收起 / 展开**面板;标题徽标显示查询命中的任务总数(列表最多展示 50 条,按更新时间倒序)
21
+ - 🏷️ 每条任务附状态徽章(按状态分类着色:新建 / 进行中 / 其他)、优先级与类型标签
19
22
  - 🔗 任务可点击,在新标签页打开 JIRA 详情
20
23
  - 🎨 颜色使用 DSH 主题令牌,浅色 / 深色主题自适应
21
24
 
22
25
  ## 安装
23
26
 
24
- 包已发布到 npm
27
+ 包已发布到公共 npm(`dsh-jira-tasks`);仓库的发布工作流同时把同一版本发布到 GitHub Packages(作用域包名 `@liu3734/dsh-jira-tasks`)。从 npm 安装(推荐):
25
28
 
26
29
  ```bash
27
30
  dsh plugin --profile web add dsh-jira-tasks
@@ -29,6 +32,8 @@ dsh plugin --profile web add dsh-jira-tasks
29
32
 
30
33
  **重启 DSH** 后生效。
31
34
 
35
+ > 若改用 GitHub Packages 源:先在 profile 的 `.npmrc` 配置 `@liu3734:registry=https://npm.pkg.github.com/` 及读取令牌,再执行 `dsh plugin --profile web add @liu3734/dsh-jira-tasks`。
36
+
32
37
  <details>
33
38
  <summary>手动安装(不使用 npm)</summary>
34
39
 
@@ -52,7 +57,14 @@ dsh plugin --profile web add dsh-jira-tasks
52
57
 
53
58
  ### 1. JIRA 地址与令牌
54
59
 
55
- 写入 `$DSH_HOME/.credentials.yaml`(推荐,热加载无需重启),或启动 DSH 前导出环境变量:
60
+ 打开 **设置 插件 → 插件配置 → JIRA**,填写:
61
+
62
+ - **JIRA 地址**:如 `http://jira.example.com/`(存入用户设置文档,可在界面回读)
63
+ - **访问令牌 / PAT**:写入凭据存储(`$DSH_HOME/.credentials.yaml`),前端只显示"已配置",不回传令牌本身
64
+
65
+ 留空并保存会保持已有令牌不变;地址留空并保存则清除设置项,回退到环境变量。认证自动识别:令牌含 `:` 用 Basic,否则用 Bearer(JIRA PAT)。
66
+
67
+ 以下环境变量 / 凭据仍作为**回退**(设置页未配置时生效,兼容旧部署),热加载无需重启:
56
68
 
57
69
  ```yaml
58
70
  JIRA_BASE_URL: "http://jira.example.com/"
@@ -61,7 +73,6 @@ JIRA_API_TOKEN: "<PAT 或 user:token>"
61
73
 
62
74
  - 地址别名:`JIRA_BASE_URL` / `JIRA_URL`
63
75
  - 令牌别名:`JIRA_API_TOKEN` / `JIRA_TOKEN`
64
- - 认证自动识别:令牌含 `:` 用 Basic,否则用 Bearer(JIRA PAT)
65
76
 
66
77
  ### 2. 项目 Key 与 JQL(按工作区)
67
78
 
@@ -88,11 +99,15 @@ dsh plugin --profile web remove dsh-jira-tasks
88
99
  <details>
89
100
  <summary>面板显示「查询失败」</summary>
90
101
 
102
+ 面板中的提示位于「查询失败:」之后,与下列文案对应:
103
+
91
104
  | 提示 | 处理 |
92
105
  |---|---|
93
- | 未配置环境变量 JIRA_BASE_URL | 凭据未写入,见上文「配置 1」 |
106
+ | 未设置项目 Key | 面板未配置项目,点标题右侧 填写项目 Key |
107
+ | 未配置 JIRA 地址(设置 → 插件 → JIRA,或环境变量 JIRA_BASE_URL) | 地址未写入,见上文「配置 1」 |
108
+ | 未配置 JIRA 令牌(设置 → 插件 → JIRA,或环境变量 JIRA_API_TOKEN) | 令牌未写入,见上文「配置 1」 |
94
109
  | 401 … | 令牌无效或认证方式不对;先 `curl -H "Authorization: Bearer <token>" <base>/rest/api/2/myself` 验证 |
95
- | 无法解析 JIRA 响应 | 网络 / 代理问题,curl 无输出 |
110
+ | 无法解析 JIRA 响应:… | 网络 / 代理问题,curl 无输出 |
96
111
  </details>
97
112
 
98
113
  <details>
@@ -108,20 +123,21 @@ dsh plugin --profile web remove dsh-jira-tasks
108
123
  <summary>点击展开</summary>
109
124
 
110
125
  ```
111
- ┌─────────── 浏览器(Client) ───────────┐ ┌──────────── Host ──────────────┐
112
- │ conversation.composer.dock(活跃会话) │ webServer 路由 /jira/api/search
113
- │ conversation.input.dock(新会话, order:99)│ │ ↓
114
- 挂载时/刷新时 fetch POST credentials.resolve(JIRA_*)
115
- 渲染:任务列表 / 错误 / 未配置 subprocess.spawn(curl …)
116
- localStorage 按工作区存取配置 stdout JSON
117
- └────────────────────────────────────────────┘ 解析 issues 返回 {ok,issues}
118
- └────────────────────────────────┘
126
+ ┌────────────────────────────────────────┐ ┌────────────────────────────────────┐
127
+ │ conversation.composer.dock(活跃会话) │ webServer 路由 /jira/api/search
128
+ │ conversation.input.dock(新会话)
129
+ 面板 hero 布局(flex order:99) settings.get("jira-tasks").baseUrl
130
+ 挂载 / 刷新时 fetch POST credentials.resolve(JIRA_API_TOKEN)
131
+ 渲染:任务列表 / 错误 / 未配置 subprocess.spawn(curl …)
132
+ localStorage 按工作区存取项目 Key/JQL │ │ stdout JSON
133
+ │ settings.plugin.item(设置页卡片) │ │ 解析 issues → 返回 {ok,issues} │
134
+ └────────────────────────────────────────┘ └────────────────────────────────────┘
119
135
  ```
120
136
 
121
- - **Host**:注册 `webServer` 路由 `POST /jira/api/search`;凭据经 `credentials` 服务解析(环境变量 / `$DSH_HOME/.credentials.yaml`,热加载);查询用 `subprocess` 直接 `spawn curl`,认证头经 stdin(`--config -`)传入,令牌不进入命令行参数。
122
- - **Client**:`window.__ModuleLoader__.load({ id, factory })` 标准 web bundle;注册 `conversation.composer.dock`(活跃会话)与 `conversation.input.dock`(新会话,flex `order: 99` 置于输入框下方、与输入框等宽)。
137
+ - **Host**:注册 `settings` 命名空间 `jira-tasks`(`baseUrl`,可读)与 `webServer` 路由 `POST /jira/api/search`;地址优先读设置文档,令牌经 `credentials` 服务解析(设置页写入 / 环境变量 / `$DSH_HOME/.credentials.yaml`,热加载);查询用 `subprocess` 直接 `spawn curl`,认证头经 stdin(`--config -`)传入,令牌不进入命令行参数。
138
+ - **Client**:`window.__ModuleLoader__.load({ id, factory })` 标准 web bundle;注册 `conversation.composer.dock`(活跃会话,注册 `order: 5`)与 `conversation.input.dock`(新会话,注册 `order: 10`)。新会话面板走 hero 布局:面板元素自身 `flex order: 99` 排在输入框之后下方,并以 `--dsh-composer-side-clearance` / `--dsh-composer-card-max-width` 与输入卡等宽。另注册 `settings.plugin.item`(`key: "jira-tasks"`)作为设置页卡片:地址经 `settingsScope` 写入命名空间,令牌经 `remote.credentials` 写入凭据存储。
123
139
  - **为什么不用 `shell` 服务**:`shell` 会套 `sandbox-exec`,部分 macOS 上不可用(`sandbox_apply: Operation not permitted`);`subprocess` 是原始进程缝,无此问题。
124
- - **新会话显示**:DSH 壳在 hero(空白会话)阶段不渲染 `composer.dock`,故额外注册 `input.dock`,并用「会话是否已有消息」去重,避免双份面板。
140
+ - **新会话显示**:DSH 壳在 hero(空白会话)阶段不渲染 `composer.dock`,故额外注册 `input.dock`,并用「会话是否已有消息」去重(新版 DSH 依据 `SessionSnapshot.blank`),避免双份面板。
125
141
 
126
142
  **与动态插件版的差异**
127
143
 
@@ -130,7 +146,7 @@ dsh plugin --profile web remove dsh-jira-tasks
130
146
  | 持久性 | 重启丢失 | 重启保留 |
131
147
  | Client→Host 通信 | `host.call` / `harness.handle` | `webServer` 路由 + `fetch` |
132
148
  | 客户端 bundle | 会话内注入 | `/plugins/dsh-jira-tasks/client.js` |
133
- | 配置 / 凭据 | 同一 `localStorage` 键、同一 `.credentials.yaml` | 完全相同 |
149
+ | 配置 / 凭据 | 仅环境变量 / `.credentials.yaml`(无设置页卡片) | 设置页卡片 + 同一 `.credentials.yaml` 回退 |
134
150
  </details>
135
151
 
136
152
  ## License
package/lib/client.js CHANGED
@@ -43,7 +43,11 @@ window.__ModuleLoader__.load({
43
43
  + ".jt-btn:hover{background:var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.1));opacity:1}"
44
44
  + ".jt-btn-primary{background:var(--dsw-alias-button-primary-fill, #2f6fed);border-color:transparent;color:var(--dsw-alias-label-primary-foreground, #fff)}"
45
45
  + ".jt-btn-primary:hover{background:var(--dsw-alias-button-primary-hover, #3a7bfd)}"
46
- + ".jt-linklike{background:none;border:none;padding:0;cursor:pointer;color:var(--dsw-alias-brand-primary, #2f6fed);font-size:12px;text-decoration:underline}";
46
+ + ".jt-linklike{background:none;border:none;padding:0;cursor:pointer;color:var(--dsw-alias-brand-primary, #2f6fed);font-size:12px;text-decoration:underline}"
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
+ + ".jt-set-head{display:flex;flex-direction:column;gap:2px;margin-bottom:8px}"
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}";
47
51
 
48
52
  var STORAGE_KEY = "dsh.jiraTasks.config.v1";
49
53
 
@@ -264,6 +268,147 @@ window.__ModuleLoader__.load({
264
268
  );
265
269
  }
266
270
 
271
+ // ---- 设置页卡片:设置 → 插件 → JIRA ----
272
+ // 地址存 settings 命名空间(可读);令牌写 credentials(永不随响应回传)。
273
+ var SETTINGS_NS = "jira-tasks";
274
+ var TOKEN_REF = "JIRA_API_TOKEN";
275
+ var TOKEN_REFS = ["JIRA_API_TOKEN", "JIRA_TOKEN"];
276
+
277
+ function useScopeSnapshot(scope) {
278
+ var state = React.useState(function () { return scope.getSnapshot(); });
279
+ var snapshot = state[0], setSnapshot = state[1];
280
+ React.useEffect(function () {
281
+ var update = function () { setSnapshot(scope.getSnapshot()); };
282
+ var off = scope.subscribe(update);
283
+ update();
284
+ return off;
285
+ }, []);
286
+ return snapshot;
287
+ }
288
+
289
+ function createJiraSettingsCard(scope, credentials) {
290
+ function readTokenInfo() {
291
+ return credentials.describe(TOKEN_REFS).then(function (res) {
292
+ if (!res || !res.ok || !res.value) return { configured: false, writable: false };
293
+ 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 };
296
+ }
297
+ var primary = res.value[TOKEN_REF];
298
+ return { configured: false, writable: primary ? primary.writable !== false : true };
299
+ }).catch(function () {
300
+ return { configured: false, writable: false };
301
+ });
302
+ }
303
+
304
+ return function JiraSettingsCard() {
305
+ var snapshot = useScopeSnapshot(scope);
306
+ var tokenInfoState = React.useState(null);
307
+ var tokenInfo = tokenInfoState[0], setTokenInfo = tokenInfoState[1];
308
+ var baseEditState = React.useState(undefined);
309
+ var baseEdit = baseEditState[0], setBaseEdit = baseEditState[1];
310
+ var tokenDraftState = React.useState("");
311
+ var tokenDraft = tokenDraftState[0], setTokenDraft = tokenDraftState[1];
312
+ var busyState = React.useState(false);
313
+ var busy = busyState[0], setBusy = busyState[1];
314
+ var failedState = React.useState("");
315
+ var failed = failedState[0], setFailed = failedState[1];
316
+
317
+ React.useEffect(function () {
318
+ var alive = true;
319
+ readTokenInfo().then(function (info) { if (alive) setTokenInfo(info); });
320
+ return function () { alive = false; };
321
+ }, []);
322
+
323
+ var storedBase = snapshot.value && typeof snapshot.value.baseUrl === "string" ? snapshot.value.baseUrl : "";
324
+ var baseValue = baseEdit === undefined ? storedBase : baseEdit;
325
+ var dirty = (baseEdit !== undefined && baseEdit !== storedBase) || (tokenDraft || "").length > 0;
326
+ var writable = snapshot.writable !== false;
327
+
328
+ // 命名空间未挂载时不显示,避免留下一张无法操作的卡片。
329
+ if (snapshot.status === "unavailable") return null;
330
+
331
+ function save() {
332
+ if (busy || !dirty) return;
333
+ setBusy(true);
334
+ setFailed("");
335
+ var tasks = [];
336
+ if (baseEdit !== undefined && baseEdit !== storedBase) {
337
+ tasks.push(baseEdit === "" ? scope.unset("baseUrl") : scope.set("baseUrl", baseEdit));
338
+ }
339
+ var token = (tokenDraft || "").trim();
340
+ if (token) {
341
+ tasks.push(credentials.set(TOKEN_REF, token).then(function (res) {
342
+ if (res && res.ok === false) {
343
+ var err = res.error;
344
+ throw new Error((err && (err.message || err.code)) || "令牌保存失败");
345
+ }
346
+ }));
347
+ }
348
+ Promise.all(tasks).then(function () {
349
+ return readTokenInfo();
350
+ }).then(function (info) {
351
+ setTokenInfo(info);
352
+ setBaseEdit(undefined);
353
+ setTokenDraft("");
354
+ setBusy(false);
355
+ }).catch(function (err) {
356
+ setBusy(false);
357
+ setFailed(String((err && err.message) || err));
358
+ });
359
+ }
360
+
361
+ function discard() {
362
+ setBaseEdit(undefined);
363
+ setTokenDraft("");
364
+ setFailed("");
365
+ }
366
+
367
+ var tokenHint = tokenInfo === null
368
+ ? "正在读取令牌状态…"
369
+ : (tokenInfo.configured ? "令牌已配置;留空并保存可保持不变。" : "令牌未配置。含 “:” 时用 Basic,否则用 Bearer。");
370
+
371
+ return h("li", { className: "jt-set-card" },
372
+ h("div", { className: "jt-set-head" },
373
+ h("span", { className: "jt-set-title" }, "JIRA"),
374
+ h("span", { className: "jt-set-desc" }, "JIRA 地址与访问令牌。项目 Key / JQL 仍按工作区在会话面板的 ⚙ 中配置。")
375
+ ),
376
+ h("div", { className: "jt-field" },
377
+ h("label", { htmlFor: "jira-tasks-base-url" }, "JIRA 地址"),
378
+ h("input", {
379
+ id: "jira-tasks-base-url",
380
+ className: "jt-input",
381
+ value: baseValue,
382
+ placeholder: "http://jira.example.com/",
383
+ disabled: !writable || busy,
384
+ onChange: function (e) { setBaseEdit(e.target.value); },
385
+ onKeyDown: function (e) { if (e.key === "Enter") save(); }
386
+ })
387
+ ),
388
+ h("div", { className: "jt-field" },
389
+ h("label", { htmlFor: "jira-tasks-token" }, "访问令牌 / PAT"),
390
+ h("input", {
391
+ id: "jira-tasks-token",
392
+ className: "jt-input",
393
+ type: "password",
394
+ value: tokenDraft,
395
+ placeholder: tokenInfo && tokenInfo.configured ? "已配置,留空保持不变" : "粘贴令牌",
396
+ autoComplete: "off",
397
+ disabled: !writable || busy,
398
+ onChange: function (e) { setTokenDraft(e.target.value); },
399
+ onKeyDown: function (e) { if (e.key === "Enter") save(); }
400
+ })
401
+ ),
402
+ h("div", { className: "jt-hint", style: { marginBottom: "8px" } }, tokenHint),
403
+ failed ? h("div", { className: "jt-error", style: { marginBottom: "8px" } }, "保存失败:" + failed) : null,
404
+ h("div", { className: "jt-buttons" },
405
+ 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 ? "保存中…" : "保存")
407
+ )
408
+ );
409
+ };
410
+ }
411
+
267
412
  function apply(ctx) {
268
413
  // ctx.effect(cb) 会立即执行 cb 并把其返回值作为卸载时的清理函数,
269
414
  // 所以样式注入必须放在回调内、返回移除函数,否则标签刚插入就被删除。
@@ -289,9 +434,19 @@ window.__ModuleLoader__.load({
289
434
  return h(JiraTasksDock, Object.assign({}, props, { blankOnly: true }));
290
435
  });
291
436
  });
437
+
438
+ // 设置页卡片:宿主注册了 jira-tasks 命名空间时,由插件配置 Tab 分发到本卡片。
439
+ var settingsScope = ctx.get("settingsScope");
440
+ var remote = ctx.get("remote");
441
+ if (settingsScope && remote && remote.credentials) {
442
+ var card = createJiraSettingsCard(settingsScope.bind({ namespace: SETTINGS_NS }), remote.credentials);
443
+ slots.inject("settings.plugin.item", function () {
444
+ return slots.register({ name: "settings.plugin.item", key: SETTINGS_NS }, card);
445
+ });
446
+ }
292
447
  }
293
448
 
294
- exports.inject = ["slots"];
449
+ exports.inject = ["slots", "settingsScope", "remote", "remote.credentials"];
295
450
  exports.apply = apply;
296
451
  return module.exports;
297
452
  }
package/lib/index.js CHANGED
@@ -1,9 +1,25 @@
1
1
  /**
2
2
  * dsh-jira-tasks — HOST half (persistent profile plugin).
3
- * Serves POST /jira/api/search: reads JIRA_BASE_URL / JIRA_API_TOKEN via the
4
- * credentials service, queries JIRA /rest/api/2/search through subprocess+curl
5
- * (auth header via stdin --config -), returns normalized issue JSON.
3
+ * Serves POST /jira/api/search: reads the JIRA base URL from the `jira-tasks`
4
+ * settings namespace and the token from the credentials service (falling back
5
+ * to JIRA_BASE_URL / JIRA_API_TOKEN for existing setups), queries JIRA
6
+ * /rest/api/2/search through subprocess+curl (auth header via stdin
7
+ * --config -), returns normalized issue JSON.
6
8
  */
9
+ import z from '@deepseek-ai/schemastery';
10
+
11
+ /** Settings namespace exposing the editable connection fields in the GUI. */
12
+ const SETTINGS_NS = 'jira-tasks';
13
+
14
+ /**
15
+ * Durable connection schema. `baseUrl` is a plain readable field; the token is
16
+ * deliberately NOT stored here — it stays in the credentials store (write-only
17
+ * from the client) so no secret ever lands in settings.yaml.
18
+ */
19
+ const JiraSettingsSchema = z.object({
20
+ baseUrl: z.string().default('').description('JIRA 地址,例如 http://jira.example.com/')
21
+ });
22
+
7
23
  export default {
8
24
  inject: ['subprocess', 'credentials', 'sandboxPolicy', 'webServer'],
9
25
  apply(ctx) {
@@ -39,6 +55,24 @@ export default {
39
55
  return undefined;
40
56
  }
41
57
 
58
+ // Register the GUI-editable section when the optional settings service is
59
+ // composed; without it the plugin keeps working from the composition/env.
60
+ ctx.inject(['settings'], (settingsCtx) => {
61
+ settingsCtx.settings.register(SETTINGS_NS, JiraSettingsSchema);
62
+ });
63
+
64
+ /** Base URL from the settings document, or '' when unset/unavailable. */
65
+ function settingsBaseUrl() {
66
+ try {
67
+ const settings = ctx.get('settings');
68
+ if (!settings) return '';
69
+ const section = settings.get(SETTINGS_NS);
70
+ return section && typeof section.baseUrl === 'string' ? section.baseUrl.trim() : '';
71
+ } catch (e) {
72
+ return '';
73
+ }
74
+ }
75
+
42
76
  function buildAuthHeader(token) {
43
77
  if (token.indexOf(':') !== -1) return 'Basic ' + Buffer.from(token, 'utf-8').toString('base64');
44
78
  return 'Bearer ' + token;
@@ -87,10 +121,10 @@ export default {
87
121
  const projectKey = String((args && args.projectKey) || '').trim();
88
122
  if (!projectKey) return { ok: false, error: '未设置项目 Key' };
89
123
  try {
90
- const baseUrl = await resolveFirst(['JIRA_BASE_URL', 'JIRA_URL']);
91
- if (!baseUrl) return { ok: false, error: '未配置环境变量 JIRA_BASE_URL(或 JIRA_URL)' };
124
+ const baseUrl = settingsBaseUrl() || await resolveFirst(['JIRA_BASE_URL', 'JIRA_URL']);
125
+ if (!baseUrl) return { ok: false, error: '未配置 JIRA 地址(设置 → 插件 → JIRA,或环境变量 JIRA_BASE_URL)' };
92
126
  const token = await resolveFirst(['JIRA_API_TOKEN', 'JIRA_TOKEN']);
93
- if (!token) return { ok: false, error: '未配置环境变量 JIRA_API_TOKEN(或 JIRA_TOKEN)' };
127
+ if (!token) return { ok: false, error: '未配置 JIRA 令牌(设置 → 插件 → JIRA,或环境变量 JIRA_API_TOKEN)' };
94
128
 
95
129
  const jql = buildJql(args && args.jql ? String(args.jql) : '', projectKey);
96
130
  const raw = await queryJira(baseUrl, token, jql);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-jira-tasks",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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",
@@ -9,6 +9,9 @@
9
9
  "./client": "./lib/client.js",
10
10
  "./package.json": "./package.json"
11
11
  },
12
+ "dependencies": {
13
+ "@deepseek-ai/schemastery": "^3.18.2"
14
+ },
12
15
  "files": [
13
16
  "lib",
14
17
  "cordis.patch.yml",
@@ -36,7 +39,9 @@
36
39
  "client": {
37
40
  "platform": "web",
38
41
  "inject": [
39
- "@deepseek-ai/dsh-client-ui-conversation"
42
+ "@deepseek-ai/dsh-client-ui-conversation",
43
+ "@deepseek-ai/dsh-client-ui-settings",
44
+ "@deepseek-ai/dsh-api-remotes"
40
45
  ]
41
46
  }
42
47
  }