dsh-jira-tasks 1.0.6 → 1.0.8
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 +33 -5
- package/README.md +37 -6
- package/lib/client.js +95 -19
- package/lib/index.js +16 -6
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -25,12 +25,18 @@ Published to npm:
|
|
|
25
25
|
dsh plugin --profile web add dsh-jira-tasks
|
|
26
26
|
```
|
|
27
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
|
+
|
|
28
34
|
**Restart DSH** to activate.
|
|
29
35
|
|
|
30
36
|
<details>
|
|
31
37
|
<summary>Manual install (without npm)</summary>
|
|
32
38
|
|
|
33
|
-
1. Copy the
|
|
39
|
+
1. Copy this repository (its root is the package directory) to `~/.dsh/profiles/web/packages/dsh-jira-tasks/` (skip `.git/`)
|
|
34
40
|
2. Edit `~/.dsh/profiles/web/package.json`:
|
|
35
41
|
- Add to `dependencies`: `"dsh-jira-tasks": "file:./packages/dsh-jira-tasks"`
|
|
36
42
|
- Append to `dsh.profile.bundles`: `"dsh-jira-tasks"`
|
|
@@ -53,10 +59,18 @@ In a DSH session, use the Cordis tools: `cordis_define` (`kind: new`, `idPrefix:
|
|
|
53
59
|
Open **Settings → Plugins → Plugin configuration → JIRA** and fill in:
|
|
54
60
|
|
|
55
61
|
- **JIRA base URL**: e.g. `http://jira.example.com/` (stored in the user settings document and read back by the form)
|
|
56
|
-
- **Access token / PAT**: written to the credential store (`$DSH_HOME/.credentials.yaml`)
|
|
62
|
+
- **Access token / PAT**: written to the credential store (`$DSH_HOME/.credentials.yaml`) under the plugin-owned ref `JIRA_TASKS_TOKEN`; the browser only ever sees "configured", never the token itself
|
|
57
63
|
|
|
58
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).
|
|
59
65
|
|
|
66
|
+
> **The settings-page token takes precedence over the environment.** When the environment that launched DSH already defines `JIRA_API_TOKEN` (a Windows *user-level* variable counts), the card is still editable: it stores the token in its own ref `JIRA_TASKS_TOKEN`, which DSH accepts (it only refuses to write a ref the launching environment shadows), and the Host resolves tokens in this order:
|
|
67
|
+
>
|
|
68
|
+
> ```
|
|
69
|
+
> JIRA_TASKS_TOKEN (settings card) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
70
|
+
> ```
|
|
71
|
+
>
|
|
72
|
+
> Click **Clear settings token** in the card to fall back to the environment variable again.
|
|
73
|
+
|
|
60
74
|
#### Connection test
|
|
61
75
|
|
|
62
76
|
The card's footer carries a status light and a **Test connection** button:
|
|
@@ -73,7 +87,7 @@ JIRA_API_TOKEN: "<PAT or user:token>"
|
|
|
73
87
|
```
|
|
74
88
|
|
|
75
89
|
- Base URL aliases: `JIRA_BASE_URL` / `JIRA_URL`
|
|
76
|
-
- Token
|
|
90
|
+
- Token resolution order: `JIRA_TASKS_TOKEN` (written by the settings card) → `JIRA_API_TOKEN` → `JIRA_TOKEN`; the first match wins
|
|
77
91
|
|
|
78
92
|
### 2. Project key and JQL (per workspace)
|
|
79
93
|
|
|
@@ -108,6 +122,20 @@ dsh plugin --profile web remove dsh-jira-tasks
|
|
|
108
122
|
| Cannot parse JIRA response | Network / proxy issue, curl produced no output |
|
|
109
123
|
</details>
|
|
110
124
|
|
|
125
|
+
<details>
|
|
126
|
+
<summary>Can the settings card override the token from the environment?</summary>
|
|
127
|
+
|
|
128
|
+
**Yes.** The card stores the token under the plugin-owned ref `JIRA_TASKS_TOKEN` instead of writing the environment's `JIRA_API_TOKEN`. DSH only refuses to write a ref the launching environment *shadows*, so its own ref is always writable: even with `JIRA_API_TOKEN` exported by the shell or the OS, the field accepts input, the save succeeds, and the Host prefers it:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
JIRA_TASKS_TOKEN (settings card) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- The card names the effective source: with a saved token it says "overrides environment variable JIRA_API_TOKEN"; without one it says "currently using environment variable JIRA_API_TOKEN — fill in and save to override"
|
|
135
|
+
- Saving re-probes the connection; **Clear settings token (fall back to environment)** removes the override
|
|
136
|
+
- The one remaining case that reports `is supplied read-only by the launching environment` is someone exporting `JIRA_TASKS_TOKEN` itself — that ref really is read-only then; remove it (Windows: System Properties → Environment Variables, or PowerShell `[Environment]::SetEnvironmentVariable('JIRA_TASKS_TOKEN', $null, 'User')`) and restart DSH
|
|
137
|
+
</details>
|
|
138
|
+
|
|
111
139
|
<details>
|
|
112
140
|
<summary>Panel does not appear</summary>
|
|
113
141
|
|
|
@@ -125,14 +153,14 @@ dsh plugin --profile web remove dsh-jira-tasks
|
|
|
125
153
|
│ conversation.composer.dock (active) │ │ webServer route /jira/api/search │
|
|
126
154
|
│ conversation.input.dock (new, order:99) │ │ ↓ │
|
|
127
155
|
│ ↓ on mount/refresh fetch POST │ │ settings.get("jira-tasks") │
|
|
128
|
-
│ render: list / error / unconfigured │ │ credentials.resolve(
|
|
156
|
+
│ render: list / error / unconfigured │ │ credentials.resolve(TOKEN_REFS) │
|
|
129
157
|
│ localStorage per-workspace key/JQL │ │ subprocess.spawn(curl …) │
|
|
130
158
|
│ settings.plugin.item (Settings card) │ │ ↓ stdout JSON │
|
|
131
159
|
└────────────────────────────────────────────┘ │ parse issues → {ok,issues} │
|
|
132
160
|
└────────────────────────────────┘
|
|
133
161
|
```
|
|
134
162
|
|
|
135
|
-
- **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
|
|
163
|
+
- **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 resolved in the order `JIRA_TASKS_TOKEN` (written by the Settings card) → `JIRA_API_TOKEN` → `JIRA_TOKEN` (`$DSH_HOME/.credentials.yaml` / environment, hot-reloaded), so a saved card token overrides the environment; queries run through `subprocess` spawning `curl` directly, with the auth header passed via stdin (`--config -`) so the token never appears in argv.
|
|
136
164
|
- **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`.
|
|
137
165
|
- **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.
|
|
138
166
|
- **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".
|
package/README.md
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
- 👤 默认仅显示当前用户(`assignee = currentUser()`)的「开启 / 重新开启」任务
|
|
17
17
|
- ⚙️ **设置 → 插件 → JIRA** 配置 JIRA 地址与访问令牌(令牌写入凭据存储,不回传前端)
|
|
18
18
|
- 🟢 设置卡片自动探测连接并显示状态灯:绿=可用、红=不可用、灰=未配置;点「测试连接」可用**未保存的草稿值**即时验证
|
|
19
|
+
|
|
20
|
+
<img width="1590" height="1574" alt="image" src="https://github.com/user-attachments/assets/af5f11c6-aa9a-4c3b-84ca-cff6c0017b22" />
|
|
21
|
+
|
|
19
22
|
- ⚙️ 项目 Key 与 JQL 按工作区保存;未配置的工作区显示"未配置"
|
|
20
23
|
- 🔄 打开新会话自动查询,面板内支持一键刷新(⟳)
|
|
21
24
|
- 🧭 点击标题可**收起 / 展开**面板;标题徽标显示查询命中的任务总数(列表最多展示 50 条,按更新时间倒序)
|
|
@@ -31,6 +34,12 @@
|
|
|
31
34
|
dsh plugin --profile web add dsh-jira-tasks
|
|
32
35
|
```
|
|
33
36
|
|
|
37
|
+
或直接从 GitHub 安装(仓库根即包目录,`lib/` 为预构建产物):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
dsh plugin --profile web add github:liu3734/jira-tasks-dsh-plugin
|
|
41
|
+
```
|
|
42
|
+
|
|
34
43
|
**重启 DSH** 后生效。
|
|
35
44
|
|
|
36
45
|
> 若改用 GitHub Packages 源:先在 profile 的 `.npmrc` 配置 `@liu3734:registry=https://npm.pkg.github.com/` 及读取令牌,再执行 `dsh plugin --profile web add @liu3734/dsh-jira-tasks`。
|
|
@@ -38,7 +47,7 @@ dsh plugin --profile web add dsh-jira-tasks
|
|
|
38
47
|
<details>
|
|
39
48
|
<summary>手动安装(不使用 npm)</summary>
|
|
40
49
|
|
|
41
|
-
1.
|
|
50
|
+
1. 将本仓库(仓库根目录即包目录)复制为 `~/.dsh/profiles/web/packages/dsh-jira-tasks/`(可忽略 `.git/`)
|
|
42
51
|
2. 编辑 `~/.dsh/profiles/web/package.json`:
|
|
43
52
|
- `dependencies` 增加:`"dsh-jira-tasks": "file:./packages/dsh-jira-tasks"`
|
|
44
53
|
- `dsh.profile.bundles` 追加:`"dsh-jira-tasks"`
|
|
@@ -61,10 +70,18 @@ dsh plugin --profile web add dsh-jira-tasks
|
|
|
61
70
|
打开 **设置 → 插件 → 插件配置 → JIRA**,填写:
|
|
62
71
|
|
|
63
72
|
- **JIRA 地址**:如 `http://jira.example.com/`(存入用户设置文档,可在界面回读)
|
|
64
|
-
- **访问令牌 / PAT**:写入凭据存储(`$DSH_HOME/.credentials.yaml`),前端只显示"已配置",不回传令牌本身
|
|
73
|
+
- **访问令牌 / PAT**:写入凭据存储(`$DSH_HOME/.credentials.yaml`,引用名是插件自有的 `JIRA_TASKS_TOKEN`),前端只显示"已配置",不回传令牌本身
|
|
65
74
|
|
|
66
75
|
留空并保存会保持已有令牌不变;地址留空并保存则清除设置项,回退到环境变量。认证自动识别:令牌含 `:` 用 Basic,否则用 Bearer(JIRA PAT)。
|
|
67
76
|
|
|
77
|
+
> **设置页令牌优先于环境变量**:启动 DSH 的环境里已有 `JIRA_API_TOKEN`(Windows 用户级环境变量也算)时,卡片照常可以输入并保存——保存的是插件自有引用 `JIRA_TASKS_TOKEN`,DSH 不会拒绝(它只拒绝写入会被环境遮蔽的同名引用),Host 解析时也把它排在环境变量之前:
|
|
78
|
+
>
|
|
79
|
+
> ```
|
|
80
|
+
> JIRA_TASKS_TOKEN(设置页) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
81
|
+
> ```
|
|
82
|
+
>
|
|
83
|
+
> 想改回用环境变量,点卡片里的「清除设置页令牌」即可。
|
|
84
|
+
|
|
68
85
|
#### 连接测试
|
|
69
86
|
|
|
70
87
|
卡片底部有连接状态灯与 **测试连接** 按钮:
|
|
@@ -81,7 +98,7 @@ JIRA_API_TOKEN: "<PAT 或 user:token>"
|
|
|
81
98
|
```
|
|
82
99
|
|
|
83
100
|
- 地址别名:`JIRA_BASE_URL` / `JIRA_URL`
|
|
84
|
-
-
|
|
101
|
+
- 令牌解析顺序:`JIRA_TASKS_TOKEN`(设置页写入)→ `JIRA_API_TOKEN` → `JIRA_TOKEN`,前者优先
|
|
85
102
|
|
|
86
103
|
### 2. 项目 Key 与 JQL(按工作区)
|
|
87
104
|
|
|
@@ -119,6 +136,20 @@ dsh plugin --profile web remove dsh-jira-tasks
|
|
|
119
136
|
| 无法解析 JIRA 响应:… | 网络 / 代理问题,curl 无输出 |
|
|
120
137
|
</details>
|
|
121
138
|
|
|
139
|
+
<details>
|
|
140
|
+
<summary>环境变量里的令牌,能不能在设置页覆盖?</summary>
|
|
141
|
+
|
|
142
|
+
**能。** 卡片把令牌保存到插件自有引用 `JIRA_TASKS_TOKEN`,而不是直接写环境变量用的 `JIRA_API_TOKEN`。DSH 只拒绝写入「会被启动环境遮蔽的同名引用」,插件自有引用不受影响,所以哪怕 shell / 系统里已导出 `JIRA_API_TOKEN`,设置页也能正常输入并保存,并且 Host 解析时优先用它:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
JIRA_TASKS_TOKEN(设置页) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- 卡片会显示当前生效来源:已保存设置页令牌时提示「优先于环境变量 JIRA_API_TOKEN」;未保存时提示「当前使用环境变量 JIRA_API_TOKEN,填写并保存即可覆盖」
|
|
149
|
+
- 保存后自动重测连接;点「清除设置页令牌(回退到环境变量)」可删除覆盖值
|
|
150
|
+
- 唯一仍会报 `is supplied read-only by the launching environment` 的情况:有人把 `JIRA_TASKS_TOKEN` 本身也导出到了启动环境——那种情况下该引用确实只读,需先移除它(Windows:系统属性 → 环境变量,或 PowerShell `[Environment]::SetEnvironmentVariable('JIRA_TASKS_TOKEN', $null, 'User')`)并重启 DSH
|
|
151
|
+
</details>
|
|
152
|
+
|
|
122
153
|
<details>
|
|
123
154
|
<summary>面板不显示</summary>
|
|
124
155
|
|
|
@@ -136,15 +167,15 @@ dsh plugin --profile web remove dsh-jira-tasks
|
|
|
136
167
|
│ conversation.composer.dock(活跃会话) │ │ webServer 路由 /jira/api/search │
|
|
137
168
|
│ conversation.input.dock(新会话) │ │ ↓ │
|
|
138
169
|
│ 面板 hero 布局(flex order:99) │ │ settings.get("jira-tasks").baseUrl │
|
|
139
|
-
│ ↓ 挂载 / 刷新时 fetch POST │ │ credentials.resolve(
|
|
170
|
+
│ ↓ 挂载 / 刷新时 fetch POST │ │ credentials.resolve(TOKEN_REFS) │
|
|
140
171
|
│ 渲染:任务列表 / 错误 / 未配置 │ │ subprocess.spawn(curl …) │
|
|
141
172
|
│ localStorage 按工作区存取项目 Key/JQL │ │ ↓ stdout JSON │
|
|
142
173
|
│ settings.plugin.item(设置页卡片) │ │ 解析 issues → 返回 {ok,issues} │
|
|
143
174
|
└────────────────────────────────────────┘ └────────────────────────────────────┘
|
|
144
175
|
```
|
|
145
176
|
|
|
146
|
-
- **Host**:注册 `settings` 命名空间 `jira-tasks`(`baseUrl`,可读)与 `webServer` 路由 `POST /jira/api/search`;地址优先读设置文档,令牌经 `credentials`
|
|
147
|
-
- **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`
|
|
177
|
+
- **Host**:注册 `settings` 命名空间 `jira-tasks`(`baseUrl`,可读)与 `webServer` 路由 `POST /jira/api/search`;地址优先读设置文档,令牌经 `credentials` 服务按 `JIRA_TASKS_TOKEN`(设置页写入)→ `JIRA_API_TOKEN` → `JIRA_TOKEN` 的顺序解析(`$DSH_HOME/.credentials.yaml` / 环境变量,热加载),因此设置页保存的令牌能覆盖环境变量;查询用 `subprocess` 直接 `spawn curl`,认证头经 stdin(`--config -`)传入,令牌不进入命令行参数。
|
|
178
|
+
- **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` 写入 `JIRA_TASKS_TOKEN`。
|
|
148
179
|
- **为什么不用 `shell` 服务**:`shell` 会套 `sandbox-exec`,部分 macOS 上不可用(`sandbox_apply: Operation not permitted`);`subprocess` 是原始进程缝,无此问题。
|
|
149
180
|
- **新会话显示**:DSH 壳在 hero(空白会话)阶段不渲染 `composer.dock`,故额外注册 `input.dock`,并用「会话是否已有消息」去重(新版 DSH 依据 `SessionSnapshot.blank`),避免双份面板。
|
|
150
181
|
|
package/lib/client.js
CHANGED
|
@@ -285,8 +285,25 @@ window.__ModuleLoader__.load({
|
|
|
285
285
|
// ---- 设置页卡片:设置 → 插件 → JIRA ----
|
|
286
286
|
// 地址存 settings 命名空间(可读);令牌写 credentials(永不随响应回传)。
|
|
287
287
|
var SETTINGS_NS = "jira-tasks";
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
// 令牌写入插件自有引用,而不是直接写 JIRA_API_TOKEN:启动环境里导出的
|
|
289
|
+
// JIRA_API_TOKEN 会让那个引用只读(DSH 拒绝写入会被环境遮蔽的引用),而
|
|
290
|
+
// 插件自有引用不受影响,因此设置页保存的值可以覆盖环境变量——优先级由
|
|
291
|
+
// Host 的解析顺序保证(JIRA_TASKS_TOKEN → JIRA_API_TOKEN → JIRA_TOKEN)。
|
|
292
|
+
var TOKEN_REF = "JIRA_TASKS_TOKEN";
|
|
293
|
+
var ENV_TOKEN_REFS = ["JIRA_API_TOKEN", "JIRA_TOKEN"];
|
|
294
|
+
var TOKEN_REFS = [TOKEN_REF].concat(ENV_TOKEN_REFS);
|
|
295
|
+
|
|
296
|
+
// credentials-local 拒绝写入“由启动环境提供”的引用时,原始信息是英文的;这里取出引用名并给出可操作的中文说明。
|
|
297
|
+
function shadowedTokenRef(message) {
|
|
298
|
+
var m = /"([^"]+)" is supplied read-only by the launching environment/.exec(String(message || ""));
|
|
299
|
+
return m ? m[1] : "";
|
|
300
|
+
}
|
|
301
|
+
function tokenReadOnlyHint(ref) {
|
|
302
|
+
var name = ref || TOKEN_REF;
|
|
303
|
+
return "环境变量 " + name + " 由启动 DSH 的终端提供且只读,无法写入该引用。"
|
|
304
|
+
+ "请先移除它(Windows:系统属性 → 环境变量,或 PowerShell "
|
|
305
|
+
+ "[Environment]::SetEnvironmentVariable('" + name + "', $null, 'User'))并重启 DSH。";
|
|
306
|
+
}
|
|
290
307
|
|
|
291
308
|
function useScopeSnapshot(scope) {
|
|
292
309
|
var state = React.useState(function () { return scope.getSnapshot(); });
|
|
@@ -303,15 +320,30 @@ window.__ModuleLoader__.load({
|
|
|
303
320
|
function createJiraSettingsCard(scope, credentials) {
|
|
304
321
|
function readTokenInfo() {
|
|
305
322
|
return credentials.describe(TOKEN_REFS).then(function (res) {
|
|
306
|
-
if (!res || !res.ok || !res.value) return { configured: false, writable:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
323
|
+
if (!res || !res.ok || !res.value) return { configured: false, writable: true };
|
|
324
|
+
var value = res.value;
|
|
325
|
+
var override = value[TOKEN_REF];
|
|
326
|
+
var overridden = !!(override && override.configured);
|
|
327
|
+
var fallbackRef = "", fallbackSource = "";
|
|
328
|
+
for (var i = 0; i < ENV_TOKEN_REFS.length; i++) {
|
|
329
|
+
var info = value[ENV_TOKEN_REFS[i]];
|
|
330
|
+
if (info && info.configured) {
|
|
331
|
+
fallbackRef = ENV_TOKEN_REFS[i];
|
|
332
|
+
fallbackSource = info.source || "";
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
310
335
|
}
|
|
311
|
-
|
|
312
|
-
|
|
336
|
+
return {
|
|
337
|
+
configured: overridden || fallbackRef !== "",
|
|
338
|
+
overridden: overridden,
|
|
339
|
+
// 只有插件自有引用可能被环境遮蔽;JIRA_API_TOKEN 只读不影响这里的写入。
|
|
340
|
+
writable: !override || override.writable !== false,
|
|
341
|
+
// 仅当回退值确实来自启动环境时才称它为“环境变量”(旧版本可能把它存在凭据文件里)。
|
|
342
|
+
envRef: fallbackSource === "env" ? fallbackRef : ""
|
|
343
|
+
};
|
|
313
344
|
}).catch(function () {
|
|
314
|
-
|
|
345
|
+
// 状态读取失败不阻断保存,交由写入路径给出真实错误。
|
|
346
|
+
return { configured: false, writable: true };
|
|
315
347
|
});
|
|
316
348
|
}
|
|
317
349
|
|
|
@@ -345,23 +377,33 @@ window.__ModuleLoader__.load({
|
|
|
345
377
|
|
|
346
378
|
var storedBase = snapshot.value && typeof snapshot.value.baseUrl === "string" ? snapshot.value.baseUrl : "";
|
|
347
379
|
var baseValue = baseEdit === undefined ? storedBase : baseEdit;
|
|
348
|
-
var
|
|
380
|
+
var baseDirty = baseEdit !== undefined && baseEdit !== storedBase;
|
|
381
|
+
var tokenDraftText = (tokenDraft || "").trim();
|
|
382
|
+
var tokenDirty = tokenDraftText.length > 0;
|
|
383
|
+
var dirty = baseDirty || tokenDirty;
|
|
349
384
|
var writable = snapshot.writable !== false;
|
|
385
|
+
// 插件自有引用通常可写;只有 JIRA_TASKS_TOKEN 本身也被导出到启动环境时才会只读。
|
|
386
|
+
var tokenWritable = !tokenInfo || tokenInfo.writable !== false;
|
|
387
|
+
var tokenOverridden = !!(tokenInfo && tokenInfo.overridden);
|
|
388
|
+
var envTokenRef = tokenInfo && tokenInfo.envRef ? tokenInfo.envRef : "";
|
|
350
389
|
|
|
351
390
|
// 命名空间未挂载时不显示,避免留下一张无法操作的卡片。
|
|
352
391
|
if (snapshot.status === "unavailable") return null;
|
|
353
392
|
|
|
354
393
|
function save() {
|
|
355
394
|
if (busy || !dirty) return;
|
|
395
|
+
if (tokenDirty && !tokenWritable) {
|
|
396
|
+
setFailed(tokenReadOnlyHint(TOKEN_REF));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
356
399
|
setBusy(true);
|
|
357
400
|
setFailed("");
|
|
358
401
|
var tasks = [];
|
|
359
|
-
if (
|
|
402
|
+
if (baseDirty) {
|
|
360
403
|
tasks.push(baseEdit === "" ? scope.unset("baseUrl") : scope.set("baseUrl", baseEdit));
|
|
361
404
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
tasks.push(credentials.set(TOKEN_REF, token).then(function (res) {
|
|
405
|
+
if (tokenDirty) {
|
|
406
|
+
tasks.push(credentials.set(TOKEN_REF, tokenDraftText).then(function (res) {
|
|
365
407
|
if (res && res.ok === false) {
|
|
366
408
|
var err = res.error;
|
|
367
409
|
throw new Error((err && (err.message || err.code)) || "令牌保存失败");
|
|
@@ -375,9 +417,35 @@ window.__ModuleLoader__.load({
|
|
|
375
417
|
setBaseEdit(undefined);
|
|
376
418
|
setTokenDraft("");
|
|
377
419
|
setBusy(false);
|
|
420
|
+
// baseUrl 变更会经 snapshot.revision 触发自动重测;仅保存令牌时手动补一次。
|
|
421
|
+
if (!baseDirty) runProbe({});
|
|
422
|
+
}).catch(function (err) {
|
|
423
|
+
setBusy(false);
|
|
424
|
+
var shadowed = shadowedTokenRef(err && err.message);
|
|
425
|
+
setFailed(shadowed ? tokenReadOnlyHint(shadowed) : String((err && err.message) || err));
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 清除设置页保存的令牌,回退到环境变量 / 凭据文件中的旧值。
|
|
430
|
+
function clearOverride() {
|
|
431
|
+
if (busy) return;
|
|
432
|
+
setBusy(true);
|
|
433
|
+
setFailed("");
|
|
434
|
+
credentials.unset(TOKEN_REF).then(function (res) {
|
|
435
|
+
if (res && res.ok === false) {
|
|
436
|
+
var err = res.error;
|
|
437
|
+
throw new Error((err && (err.message || err.code)) || "清除令牌失败");
|
|
438
|
+
}
|
|
439
|
+
return readTokenInfo();
|
|
440
|
+
}).then(function (info) {
|
|
441
|
+
setTokenInfo(info);
|
|
442
|
+
setTokenDraft("");
|
|
443
|
+
setBusy(false);
|
|
444
|
+
runProbe({});
|
|
378
445
|
}).catch(function (err) {
|
|
379
446
|
setBusy(false);
|
|
380
|
-
|
|
447
|
+
var shadowed = shadowedTokenRef(err && err.message);
|
|
448
|
+
setFailed(shadowed ? tokenReadOnlyHint(shadowed) : String((err && err.message) || err));
|
|
381
449
|
});
|
|
382
450
|
}
|
|
383
451
|
|
|
@@ -404,7 +472,11 @@ window.__ModuleLoader__.load({
|
|
|
404
472
|
|
|
405
473
|
var tokenHint = tokenInfo === null
|
|
406
474
|
? "正在读取令牌状态…"
|
|
407
|
-
: (
|
|
475
|
+
: (tokenOverridden
|
|
476
|
+
? "设置页令牌已保存" + (envTokenRef ? ",优先于环境变量 " + envTokenRef : "") + ";留空并保存可保持不变。"
|
|
477
|
+
: (envTokenRef
|
|
478
|
+
? "当前使用环境变量 " + envTokenRef + ";在下方填写并保存即可覆盖它。"
|
|
479
|
+
: (tokenInfo.configured ? "令牌已配置;留空并保存可保持不变。" : "令牌未配置。含 “:” 时用 Basic,否则用 Bearer。")));
|
|
408
480
|
var dotClass = probe.phase === "ok" ? "jt-dot jt-dot-ok"
|
|
409
481
|
: probe.phase === "fail" ? "jt-dot jt-dot-fail"
|
|
410
482
|
: probe.phase === "testing" ? "jt-dot jt-dot-testing" : "jt-dot";
|
|
@@ -433,14 +505,18 @@ window.__ModuleLoader__.load({
|
|
|
433
505
|
className: "jt-input",
|
|
434
506
|
type: "password",
|
|
435
507
|
value: tokenDraft,
|
|
436
|
-
placeholder: tokenInfo && tokenInfo.configured ? "已配置,留空保持不变" : "粘贴令牌",
|
|
508
|
+
placeholder: tokenOverridden ? "已保存,留空保持不变" : (envTokenRef ? "填写以覆盖环境变量" : (tokenInfo && tokenInfo.configured ? "已配置,留空保持不变" : "粘贴令牌")),
|
|
437
509
|
autoComplete: "off",
|
|
438
|
-
disabled:
|
|
510
|
+
disabled: busy || !tokenWritable,
|
|
439
511
|
onChange: function (e) { setTokenDraft(e.target.value); },
|
|
440
512
|
onKeyDown: function (e) { if (e.key === "Enter") save(); }
|
|
441
513
|
})
|
|
442
514
|
),
|
|
443
515
|
h("div", { className: "jt-hint", style: { marginBottom: "8px" } }, tokenHint),
|
|
516
|
+
tokenOverridden && tokenWritable
|
|
517
|
+
? h("div", { className: "jt-hint", style: { margin: "-4px 0 8px" } },
|
|
518
|
+
h("button", { className: "jt-linklike", disabled: busy, onClick: clearOverride }, "清除设置页令牌(回退到环境变量)"))
|
|
519
|
+
: null,
|
|
444
520
|
h("div", { className: "jt-probe", role: "status" },
|
|
445
521
|
h("span", { className: dotClass, "aria-hidden": "true" }),
|
|
446
522
|
h("span", null, probe.message || "未测试")
|
|
@@ -455,7 +531,7 @@ window.__ModuleLoader__.load({
|
|
|
455
531
|
onClick: function () { runProbe({ baseUrl: baseValue, token: (tokenDraft || "").trim() }); }
|
|
456
532
|
}, probe.phase === "testing" ? "测试中…" : "测试连接"),
|
|
457
533
|
h("button", { className: "jt-btn", disabled: !dirty || busy, onClick: discard }, "放弃"),
|
|
458
|
-
h("button", { className: "jt-btn jt-btn-primary", disabled: !dirty || busy || !writable, onClick: save }, busy ? "保存中…" : "保存")
|
|
534
|
+
h("button", { className: "jt-btn jt-btn-primary", disabled: !dirty || busy || (baseDirty && !writable) || (tokenDirty && !tokenWritable), onClick: save }, busy ? "保存中…" : "保存")
|
|
459
535
|
)
|
|
460
536
|
);
|
|
461
537
|
};
|
package/lib/index.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-jira-tasks — HOST half (persistent profile plugin).
|
|
3
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
|
|
5
|
-
*
|
|
6
|
-
* /rest/api/2/search through
|
|
7
|
-
* --config -),
|
|
4
|
+
* settings namespace and the token from the credentials service — the
|
|
5
|
+
* settings-page JIRA_TASKS_TOKEN first, so it overrides an exported
|
|
6
|
+
* JIRA_API_TOKEN / JIRA_TOKEN — then queries JIRA /rest/api/2/search through
|
|
7
|
+
* subprocess+curl (auth header via stdin --config -), returning normalized
|
|
8
|
+
* issue JSON.
|
|
8
9
|
*/
|
|
9
10
|
import z from '@deepseek-ai/schemastery';
|
|
10
11
|
|
|
11
12
|
/** Settings namespace exposing the editable connection fields in the GUI. */
|
|
12
13
|
const SETTINGS_NS = 'jira-tasks';
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Token references in precedence order. `JIRA_TASKS_TOKEN` is the plugin-owned
|
|
17
|
+
* ref the settings card writes to: unlike `JIRA_API_TOKEN`, the credentials
|
|
18
|
+
* provider can always store it, because the launching environment does not
|
|
19
|
+
* supply it. Listing it first is what makes a saved token override an exported
|
|
20
|
+
* `JIRA_API_TOKEN` / `JIRA_TOKEN`.
|
|
21
|
+
*/
|
|
22
|
+
const TOKEN_REFS = ['JIRA_TASKS_TOKEN', 'JIRA_API_TOKEN', 'JIRA_TOKEN'];
|
|
23
|
+
|
|
14
24
|
/**
|
|
15
25
|
* Durable connection schema. `baseUrl` is a plain readable field; the token is
|
|
16
26
|
* deliberately NOT stored here — it stays in the credentials store (write-only
|
|
@@ -166,7 +176,7 @@ export default {
|
|
|
166
176
|
try {
|
|
167
177
|
const baseUrl = settingsBaseUrl() || await resolveFirst(['JIRA_BASE_URL', 'JIRA_URL']);
|
|
168
178
|
if (!baseUrl) return { ok: false, error: '未配置 JIRA 地址(设置 → 插件 → JIRA,或环境变量 JIRA_BASE_URL)' };
|
|
169
|
-
const token = await resolveFirst(
|
|
179
|
+
const token = await resolveFirst(TOKEN_REFS);
|
|
170
180
|
if (!token) return { ok: false, error: '未配置 JIRA 令牌(设置 → 插件 → JIRA,或环境变量 JIRA_API_TOKEN)' };
|
|
171
181
|
|
|
172
182
|
const jql = buildJql(args && args.jql ? String(args.jql) : '', projectKey);
|
|
@@ -213,7 +223,7 @@ export default {
|
|
|
213
223
|
const draftToken = args && typeof args.token === 'string' ? args.token.trim() : '';
|
|
214
224
|
const baseUrl = draftBase || settingsBaseUrl() || await resolveFirst(['JIRA_BASE_URL', 'JIRA_URL']);
|
|
215
225
|
if (!baseUrl) return { ok: false, code: 'unconfigured', error: '未配置 JIRA 地址' };
|
|
216
|
-
const token = draftToken || await resolveFirst(
|
|
226
|
+
const token = draftToken || await resolveFirst(TOKEN_REFS);
|
|
217
227
|
if (!token) return { ok: false, code: 'unconfigured', error: '未配置 JIRA 令牌' };
|
|
218
228
|
|
|
219
229
|
const raw = await probeJira(baseUrl, token);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-jira-tasks",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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",
|