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 +19 -10
- package/README.md +34 -18
- package/lib/client.js +157 -2
- package/lib/index.js +40 -6
- package/package.json +7 -2
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
|
|
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
|
-
|
|
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
|
-
|
|
|
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 │ │
|
|
111
|
-
│ render: list / error / unconfigured │ │
|
|
112
|
-
│ localStorage per-workspace
|
|
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`;
|
|
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 |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
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
|
|
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
|
-
|
|
112
|
-
│ conversation.composer.dock(活跃会话)
|
|
113
|
-
│ conversation.input.dock
|
|
114
|
-
│
|
|
115
|
-
│
|
|
116
|
-
│
|
|
117
|
-
|
|
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
|
|
122
|
-
- **Client**:`window.__ModuleLoader__.load({ id, factory })` 标准 web bundle;注册 `conversation.composer.dock
|
|
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
|
-
| 配置 / 凭据 |
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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: '
|
|
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: '
|
|
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.
|
|
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
|
}
|