dsh-jira-tasks 1.0.8 → 1.2.0
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 +29 -22
- package/README.md +23 -17
- package/lib/client.js +103 -50
- package/lib/index.js +79 -28
- package/package.json +5 -2
package/README.en.md
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
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 configured in **Settings →
|
|
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 → JIRA 配置** (the settings dialog's left navigation) (`JIRA_BASE_URL` / `JIRA_API_TOKEN` act as fallback); the project key and JQL are **configured per workspace** and persisted.
|
|
8
|
+
|
|
9
|
+
> **DSH version targeted: 0.1.7 (verified on `0.1.7-rc.2`).** The client slots `conversation.input.dock` / `plugins.item` / `settings.section`, the settings service `ctx.configForms` (namespace = profile entry id `jira-tasks`), and the `ctx.effect` / `configForms.whileServed` disposal contracts all follow the 0.1.7 interfaces; the 0.1.6-era `settingsScope` / `settings.register` / `settings.plugin.item` APIs are gone and no longer used.
|
|
8
10
|
|
|
9
11
|
## Features
|
|
10
12
|
|
|
11
13
|
- 📋 Panel shown below the composer in both new and active sessions (aligned with the input width in new sessions)
|
|
12
14
|
- 👤 Defaults to the current user (`assignee = currentUser()`) with status `开启 / 重新开启` (Open / Reopened)
|
|
13
|
-
- ⚙️ **Settings →
|
|
15
|
+
- ⚙️ **Settings → JIRA 配置** (left navigation, below Agent presets) edits the base URL and access token (the token is written to the credential store and never sent back to the browser); the same JIRA card stays available in the Plugins panel
|
|
14
16
|
- 🟢 The card auto-probes the connection and shows a status light: green = usable, red = unusable, grey = unconfigured; **Test connection** verifies unsaved drafts immediately
|
|
15
17
|
- ⚙️ Project key and JQL are saved per workspace; unconfigured workspaces show "unconfigured"
|
|
16
18
|
- 🔄 Auto-query on every new session, with a one-click refresh (⟳)
|
|
@@ -33,6 +35,10 @@ dsh plugin --profile web add github:liu3734/jira-tasks-dsh-plugin
|
|
|
33
35
|
|
|
34
36
|
**Restart DSH** to activate.
|
|
35
37
|
|
|
38
|
+
> **Nothing shows up? Check `dsh.profile.bundles` first.** DSH mounts this package as a profile layer only when `"dsh-jira-tasks"` is listed in `dsh.profile.bundles` of `~/.dsh/profiles/web/package.json`; being in `dependencies` alone is not enough — the startup log then prints `patch: entry "jira-tasks" not found` and the plugin silently never loads. `dsh plugin --profile web add` normally appends that row, but it will not re-append when the package is already a dependency; add `"dsh-jira-tasks"` to `dsh.profile.bundles` by hand.
|
|
39
|
+
|
|
40
|
+
> Using GitHub Packages instead: configure `@liu3734:registry=https://npm.pkg.github.com/` plus a read token in the profile's `.npmrc`, then run `dsh plugin --profile web add @liu3734/dsh-jira-tasks`.
|
|
41
|
+
|
|
36
42
|
<details>
|
|
37
43
|
<summary>Manual install (without npm)</summary>
|
|
38
44
|
|
|
@@ -56,12 +62,12 @@ In a DSH session, use the Cordis tools: `cordis_define` (`kind: new`, `idPrefix:
|
|
|
56
62
|
|
|
57
63
|
### 1. JIRA base URL and token
|
|
58
64
|
|
|
59
|
-
Open **Settings →
|
|
65
|
+
Open **Settings → JIRA 配置** (the last item in the left navigation, below Agent presets; the JIRA card in the Plugins panel is the same form) and fill in:
|
|
60
66
|
|
|
61
|
-
- **JIRA base URL**: e.g. `http://jira.example.com/` (
|
|
67
|
+
- **JIRA base URL**: e.g. `http://jira.example.com/` (written to this plugin entry's `Config.baseUrl` — the `jira-tasks` row in the profile's `cordis.patch.yml` — and read back by the form; a host-refused write now surfaces as an error instead of failing silently)
|
|
62
68
|
- **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
|
|
63
69
|
|
|
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).
|
|
70
|
+
Leaving the token blank on save keeps the existing one; clearing the address on save removes the override and falls back to the credential store / environment. Auth is auto-detected: tokens containing `:` use Basic, otherwise Bearer (JIRA PAT).
|
|
65
71
|
|
|
66
72
|
> **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
73
|
>
|
|
@@ -79,7 +85,7 @@ The card's footer carries a status light and a **Test connection** button:
|
|
|
79
85
|
- **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
|
|
80
86
|
- **Test connection** probes what is currently in the fields, saved or not, so you can check before saving
|
|
81
87
|
|
|
82
|
-
Environment variables / credentials still work as a **fallback** (used when the settings card
|
|
88
|
+
Environment variables / credentials still work as a **fallback** (used when the settings card leaves `baseUrl` empty), hot-reloaded without a restart. Address precedence is `Config.baseUrl` > `JIRA_BASE_URL` > `JIRA_URL`, and `JIRA_BASE_URL` may live in the launching environment or in `.credentials.yaml` — note that a stale record (e.g. an old domain left in the credential file) becomes effective again as soon as the settings page clears the address:
|
|
83
89
|
|
|
84
90
|
```yaml
|
|
85
91
|
JIRA_BASE_URL: "http://jira.example.com/"
|
|
@@ -140,7 +146,9 @@ JIRA_TASKS_TOKEN (settings card) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
|
140
146
|
<summary>Panel does not appear</summary>
|
|
141
147
|
|
|
142
148
|
- Make sure it is installed and DSH was **restarted**; in new sessions the panel sits below the input
|
|
143
|
-
- Check the
|
|
149
|
+
- Check that `dsh.profile.bundles` in `~/.dsh/profiles/web/package.json` lists `"dsh-jira-tasks"` (the most common cause of a silent no-show — see the Install note)
|
|
150
|
+
- Check the DSH startup log: `patch: entry "jira-tasks" not found` means the profile layer was never composed; `webserver: duplicate exact route` means a route was registered twice (the plugin releases its routes through `ctx.effect`, so this points at a second copy)
|
|
151
|
+
- A browser console error `client-modules: could not load "dsh-jira-tasks"` means `/plugins/dsh-jira-tasks/client.js` was not served — confirm `exports["./client"]` resolves to the built `lib/client.js`
|
|
144
152
|
</details>
|
|
145
153
|
|
|
146
154
|
## Architecture & Implementation Details
|
|
@@ -149,21 +157,20 @@ JIRA_TASKS_TOKEN (settings card) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
|
149
157
|
<summary>Expand</summary>
|
|
150
158
|
|
|
151
159
|
```
|
|
152
|
-
|
|
153
|
-
│ conversation.
|
|
154
|
-
│
|
|
155
|
-
│
|
|
156
|
-
│ render: list / error / unconfigured
|
|
157
|
-
│ localStorage per-workspace key/JQL
|
|
158
|
-
│
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
- **
|
|
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`.
|
|
160
|
+
┌────────────── Browser (Client) ──────────────┐ ┌─────────────── Host ───────────────┐
|
|
161
|
+
│ conversation.input.dock (both states) │ │ webServer route /jira/api/search │
|
|
162
|
+
│ CSS order:99 -> below the input, full width │ │ entry Config.baseUrl (volatile) │
|
|
163
|
+
│ on mount/refresh: fetch POST │ │ credentials.resolve(TOKEN_REFS) │
|
|
164
|
+
│ render: list / error / unconfigured │ │ subprocess.spawn(curl ...) │
|
|
165
|
+
│ localStorage: per-workspace key/JQL │ │ parse stdout JSON │
|
|
166
|
+
│ plugins.item (Plugins panel card) │ │ return {ok, issues} │
|
|
167
|
+
│ settings.section (Settings nav page) │ │ │
|
|
168
|
+
└──────────────────────────────────────────────┘ └────────────────────────────────────┘
|
|
169
|
+
|
|
170
|
+
- **Host**: declares the entry's own `Config` (`baseUrl`, `volatile` — since DSH 0.1.5 a settings page is built from it; `settings.configure({ auto: false }, ctx.fiber)` turns the auto-generated page off and hands its disposer back to `ctx.effect`) plus `webServer` routes `POST /jira/api/search` and `POST /jira/api/test`, each wrapped in its own `ctx.effect` (a reload otherwise hits "duplicate route" and fails activation; non-POST requests get 405); the token comes 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.
|
|
171
|
+
- **Client**: a standard `window.__ModuleLoader__.load({ id, factory })` web bundle requiring only `"react"`; registers **only `conversation.input.dock`** (`order: 10`) — that seat renders in both new and active sessions and is a full-width row of `composerStack` (`flex-direction: column`), where the panel's own CSS `order: 99` places it below the input card at the card's width. `conversation.composer.dock` is deliberately NOT used: in 0.1.7 it renders into InputBar's `.dock` **row**, side by side with the context meter, so a full-width panel there gets its right-hand content covered by the meter. The address/token form comes from `ctx.configForms.get("jira-tasks")` and the token is written through `remote.credentials` to `JIRA_TASKS_TOKEN`; `configForms.whileServed` keeps it hidden when the host serves no such namespace. It is registered in **two places**: the Plugins-panel card `plugins.item` (`id: "jira-tasks"`, `order: 41`) and a Settings-navigation section `settings.section` (`id: "jira-tasks"`, `order: 30` — above Agent presets' 20, so it sits below it; `label: "JIRA 配置"`; ids outside the shell's `navIcon` allow-list fall back to the default gear icon). Both share one `JiraSettingsPage`, whose outer container is chosen by the external `variant: "page"` prop (`li.jt-set-card` card vs `div.jt-set-page` page) while the field block is the same array. The card's `summary` view is rendered by a dispatcher that calls **no hooks**, while the form lives in a separate `JiraSettingsPage`, so flipping `view` on one mounted instance never changes its hook count.
|
|
165
172
|
- **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.
|
|
166
|
-
- **
|
|
173
|
+
- **One registration covers both states**: `conversation.input.dock` renders whenever a session and its input exist, so new and active sessions need no separate registrations (the older `composer.dock` + blank-session de-duplication was both unnecessary under 0.1.7 and the cause of the meter-row collision).
|
|
167
174
|
|
|
168
175
|
**Differences from the dynamic version**
|
|
169
176
|
|
|
@@ -172,7 +179,7 @@ JIRA_TASKS_TOKEN (settings card) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
|
172
179
|
| Persistence | Lost on restart | Survives restart |
|
|
173
180
|
| Client→Host | `host.call` / `harness.handle` | `webServer` route + `fetch` |
|
|
174
181
|
| Client bundle | Injected per session | `/plugins/dsh-jira-tasks/client.js` |
|
|
175
|
-
| Config / credentials | Env / `.credentials.yaml` only (no Settings card) | Settings card + same `.credentials.yaml` fallback |
|
|
182
|
+
| Config / credentials | Env / `.credentials.yaml` only (no Settings card) | Settings nav page + Plugins-panel card + same `.credentials.yaml` fallback |
|
|
176
183
|
</details>
|
|
177
184
|
|
|
178
185
|
## License
|
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
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
|
+
|
|
9
|
+
> **适配的 DSH 版本:0.1.7(`0.1.7-rc.2` 实测)。** 客户端槽位 `conversation.input.dock` / `plugins.item` / `settings.section`、设置服务 `ctx.configForms`(命名空间 = profile 条目 id `jira-tasks`)、`ctx.effect` / `configForms.whileServed` 的注销契约均按 0.1.7 的接口实现;0.1.6 及更早版本的 `settingsScope` / `settings.register` / `settings.plugin.item` 已不再使用。
|
|
8
10
|
|
|
9
11
|
## 功能
|
|
10
12
|
|
|
@@ -14,7 +16,7 @@
|
|
|
14
16
|
<img width="1972" height="746" alt="image" src="https://github.com/user-attachments/assets/9f543e04-a5ad-4b86-85c8-baa2de26f44e" />
|
|
15
17
|
|
|
16
18
|
- 👤 默认仅显示当前用户(`assignee = currentUser()`)的「开启 / 重新开启」任务
|
|
17
|
-
- ⚙️ **设置 →
|
|
19
|
+
- ⚙️ **设置 → JIRA 配置**(设置对话框左侧导航,位于「Agent 预设」下方)配置 JIRA 地址与访问令牌(令牌写入凭据存储,不回传前端);插件面板里的同一张 JIRA 卡片仍然可用
|
|
18
20
|
- 🟢 设置卡片自动探测连接并显示状态灯:绿=可用、红=不可用、灰=未配置;点「测试连接」可用**未保存的草稿值**即时验证
|
|
19
21
|
|
|
20
22
|
<img width="1590" height="1574" alt="image" src="https://github.com/user-attachments/assets/af5f11c6-aa9a-4c3b-84ca-cff6c0017b22" />
|
|
@@ -42,6 +44,8 @@ dsh plugin --profile web add github:liu3734/jira-tasks-dsh-plugin
|
|
|
42
44
|
|
|
43
45
|
**重启 DSH** 后生效。
|
|
44
46
|
|
|
47
|
+
> **装完没反应?先查 `dsh.profile.bundles`。** DSH 只有当 `~/.dsh/profiles/web/package.json` 的 `dsh.profile.bundles` 里列了 `"dsh-jira-tasks"` 时才把本包当作 profile 层挂载;只出现在 `dependencies` 里不够——此时启动日志会打 `patch: entry "jira-tasks" not found`,插件静默不加载。`dsh plugin --profile web add` 一般会补上这一行,但该包已在 `dependencies` 中时重装不会重新补,手动往 `dsh.profile.bundles` 追加 `"dsh-jira-tasks"` 即可。
|
|
48
|
+
|
|
45
49
|
> 若改用 GitHub Packages 源:先在 profile 的 `.npmrc` 配置 `@liu3734:registry=https://npm.pkg.github.com/` 及读取令牌,再执行 `dsh plugin --profile web add @liu3734/dsh-jira-tasks`。
|
|
46
50
|
|
|
47
51
|
<details>
|
|
@@ -67,12 +71,12 @@ dsh plugin --profile web add github:liu3734/jira-tasks-dsh-plugin
|
|
|
67
71
|
|
|
68
72
|
### 1. JIRA 地址与令牌
|
|
69
73
|
|
|
70
|
-
打开 **设置 →
|
|
74
|
+
打开 **设置 → JIRA 配置**(左侧导航最后一项,在「Agent 预设」下面;插件面板里的 JIRA 卡片是同一张表单),填写:
|
|
71
75
|
|
|
72
|
-
- **JIRA 地址**:如 `http://jira.example.com
|
|
76
|
+
- **JIRA 地址**:如 `http://jira.example.com/`(写入本插件 profile 条目 `jira-tasks` 的 `Config.baseUrl`,即 profile 的 `cordis.patch.yml`,可在界面回读;保存被宿主拒绝时会直接报错,不会静默失败)
|
|
73
77
|
- **访问令牌 / PAT**:写入凭据存储(`$DSH_HOME/.credentials.yaml`,引用名是插件自有的 `JIRA_TASKS_TOKEN`),前端只显示"已配置",不回传令牌本身
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
留空并保存会保持已有令牌不变;地址留空并保存则清除设置项,回退到凭据存储 / 环境变量。认证自动识别:令牌含 `:` 用 Basic,否则用 Bearer(JIRA PAT)。
|
|
76
80
|
|
|
77
81
|
> **设置页令牌优先于环境变量**:启动 DSH 的环境里已有 `JIRA_API_TOKEN`(Windows 用户级环境变量也算)时,卡片照常可以输入并保存——保存的是插件自有引用 `JIRA_TASKS_TOKEN`,DSH 不会拒绝(它只拒绝写入会被环境遮蔽的同名引用),Host 解析时也把它排在环境变量之前:
|
|
78
82
|
>
|
|
@@ -90,7 +94,7 @@ dsh plugin --profile web add github:liu3734/jira-tasks-dsh-plugin
|
|
|
90
94
|
- **绿** = 地址与令牌可用(并显示当前登录用户);**红** = 不可用(显示 JIRA 返回的原因,如 401 认证失败);**灰** = 地址或令牌未配置
|
|
91
95
|
- 点 **测试连接** 会用**当前输入框里的内容**(未保存也可)立即测试,方便改完再存
|
|
92
96
|
|
|
93
|
-
以下环境变量 /
|
|
97
|
+
以下环境变量 / 凭据仍作为**回退**(设置页未配置 `baseUrl` 时生效,兼容旧部署),热加载无需重启。注意地址的优先级是 `Config.baseUrl` > `JIRA_BASE_URL` > `JIRA_URL`,而 `JIRA_BASE_URL` 既可以从启动环境读,也可以像下面这样存在 `.credentials.yaml` 里——旧域名的记录留在凭据文件里时,设置页一旦清空地址就会回退到它:
|
|
94
98
|
|
|
95
99
|
```yaml
|
|
96
100
|
JIRA_BASE_URL: "http://jira.example.com/"
|
|
@@ -130,8 +134,8 @@ dsh plugin --profile web remove dsh-jira-tasks
|
|
|
130
134
|
| 提示 | 处理 |
|
|
131
135
|
|---|---|
|
|
132
136
|
| 未设置项目 Key | 面板未配置项目,点标题右侧 ⚙ 填写项目 Key |
|
|
133
|
-
| 未配置 JIRA 地址(设置 →
|
|
134
|
-
| 未配置 JIRA 令牌(设置 →
|
|
137
|
+
| 未配置 JIRA 地址(设置 → JIRA 配置,或环境变量 JIRA_BASE_URL) | 地址未写入,见上文「配置 1」 |
|
|
138
|
+
| 未配置 JIRA 令牌(设置 → JIRA 配置,或环境变量 JIRA_API_TOKEN) | 令牌未写入,见上文「配置 1」 |
|
|
135
139
|
| 401 … | 令牌无效或认证方式不对;先 `curl -H "Authorization: Bearer <token>" <base>/rest/api/2/myself` 验证 |
|
|
136
140
|
| 无法解析 JIRA 响应:… | 网络 / 代理问题,curl 无输出 |
|
|
137
141
|
</details>
|
|
@@ -154,7 +158,9 @@ JIRA_TASKS_TOKEN(设置页) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
|
154
158
|
<summary>面板不显示</summary>
|
|
155
159
|
|
|
156
160
|
- 确认已安装并**重启 DSH**;新会话面板位于输入框下方
|
|
157
|
-
- 检查
|
|
161
|
+
- 检查 `~/.dsh/profiles/web/package.json` 的 `dsh.profile.bundles` 是否包含 `"dsh-jira-tasks"`(这是最常见的漏装原因,见「安装」一节的提示)
|
|
162
|
+
- 检查 DSH 启动日志:出现 `patch: entry "jira-tasks" not found` 即为 profile 层未挂载;出现 `webserver: duplicate exact route` 说明同一路由被注册了两次(插件自身已用 `ctx.effect` 释放旧路由,若仍有说明有第二份副本)
|
|
163
|
+
- 浏览器控制台若报 `client-modules: could not load "dsh-jira-tasks"`,说明 `/plugins/dsh-jira-tasks/client.js` 没取到——确认 `package.json` 的 `exports["./client"]` 指向已构建的 `lib/client.js`
|
|
158
164
|
</details>
|
|
159
165
|
|
|
160
166
|
## 架构与实现细节
|
|
@@ -164,20 +170,20 @@ JIRA_TASKS_TOKEN(设置页) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
|
164
170
|
|
|
165
171
|
```
|
|
166
172
|
┌────────────────────────────────────────┐ ┌────────────────────────────────────┐
|
|
167
|
-
│ conversation.
|
|
168
|
-
│
|
|
169
|
-
│ 面板 hero 布局(flex order:99) │ │ settings.get("jira-tasks").baseUrl │
|
|
173
|
+
│ conversation.input.dock(两种会话) │ │ webServer 路由 /jira/api/search │
|
|
174
|
+
│ CSS order:99 → 输入框下方、整宽 │ │ 条目 Config.baseUrl(volatile 引用) │
|
|
170
175
|
│ ↓ 挂载 / 刷新时 fetch POST │ │ credentials.resolve(TOKEN_REFS) │
|
|
171
176
|
│ 渲染:任务列表 / 错误 / 未配置 │ │ subprocess.spawn(curl …) │
|
|
172
177
|
│ localStorage 按工作区存取项目 Key/JQL │ │ ↓ stdout JSON │
|
|
173
|
-
│
|
|
178
|
+
│ plugins.item(插件面板卡片) │ │ 解析 issues → 返回 {ok,issues} │
|
|
179
|
+
│ settings.section(设置导航整页) │ │ │
|
|
174
180
|
└────────────────────────────────────────┘ └────────────────────────────────────┘
|
|
175
181
|
```
|
|
176
182
|
|
|
177
|
-
- **Host
|
|
178
|
-
- **Client**:`window.__ModuleLoader__.load({ id, factory })` 标准 web bundle
|
|
183
|
+
- **Host**:声明条目自身的 `Config`(`baseUrl`,`volatile`,DSH 0.1.5 起设置页的表单直接来自它;`settings.configure({ auto: false }, ctx.fiber)` 关掉自动生成页,并把返回的注销函数交回 `ctx.effect`)与 `webServer` 路由 `POST /jira/api/search`、`POST /jira/api/test`(两条路由各自包在 `ctx.effect` 里,卸载/重载时先释放,否则重挂载会撞上「重复路由」直接抛错;非 POST 返回 405);令牌经 `credentials` 服务按 `JIRA_TASKS_TOKEN`(设置页写入)→ `JIRA_API_TOKEN` → `JIRA_TOKEN` 的顺序解析(`$DSH_HOME/.credentials.yaml` / 环境变量,热加载),因此设置页保存的令牌能覆盖环境变量;查询用 `subprocess` 直接 `spawn curl`,认证头经 stdin(`--config -`)传入,令牌不进入命令行参数。
|
|
184
|
+
- **Client**:`window.__ModuleLoader__.load({ id, factory })` 标准 web bundle,仅 `require("react")`;**只注册 `conversation.input.dock` 一处**(`order: 10`)。该槽位在新会话与活跃会话下都会渲染,属于 `composerStack`(`flex-direction: column`)的整宽纵向行;面板元素自身 `flex order: 99` 排到输入卡之后,即输入框下方,并以 `--dsh-composer-side-clearance` / `--dsh-composer-card-max-width` 与输入卡等宽。之所以不用 `conversation.composer.dock`:0.1.7 里那一槽位渲染进 InputBar 的 `.dock`**横向** flex 行,与上下文占用环并排,整宽面板会被占用环压住右侧内容。插件地址与令牌的表单来自 `ctx.configForms.get("jira-tasks")`(地址)与 `remote.credentials`(令牌写入 `JIRA_TASKS_TOKEN`),并用 `configForms.whileServed` 保证宿主未提供该命名空间时不显示。表单**注册在两处**:插件面板卡片 `plugins.item`(`id: "jira-tasks"`,`order: 41`,原入口保留)与设置对话框左侧导航项 `settings.section`(`id: "jira-tasks"`,`order: 30` —— 大于「Agent 预设」的 20,所以显示在它下方;`label: "JIRA 配置"`;未在 `navIcon` 白名单里的 id 由设置外壳回退成默认齿轮图标)。两者共用同一个 `JiraSettingsPage`:由外部 prop `variant: "page"` 决定外层容器(`li.jt-set-card` 卡片 / `div.jt-set-page` 整页),字段区是同一份数组。`plugins.item` 的摘要位由一个**不调用任何 hook** 的分发组件负责,表单在独立的 `JiraSettingsPage` 里,避免同一实例在 `view` 切换时改变 hook 数量。
|
|
179
185
|
- **为什么不用 `shell` 服务**:`shell` 会套 `sandbox-exec`,部分 macOS 上不可用(`sandbox_apply: Operation not permitted`);`subprocess` 是原始进程缝,无此问题。
|
|
180
|
-
-
|
|
186
|
+
- **一处注册覆盖两种会话**:`conversation.input.dock` 只要有 session + input 就会渲染,新会话与活跃会话无需分别注册(旧版曾用 `composer.dock` + 空白判定去重,0.1.7 下既不必要、又会与占用环抢同一行)。
|
|
181
187
|
|
|
182
188
|
**与动态插件版的差异**
|
|
183
189
|
|
|
@@ -186,7 +192,7 @@ JIRA_TASKS_TOKEN(设置页) > JIRA_API_TOKEN > JIRA_TOKEN
|
|
|
186
192
|
| 持久性 | 重启丢失 | 重启保留 |
|
|
187
193
|
| Client→Host 通信 | `host.call` / `harness.handle` | `webServer` 路由 + `fetch` |
|
|
188
194
|
| 客户端 bundle | 会话内注入 | `/plugins/dsh-jira-tasks/client.js` |
|
|
189
|
-
| 配置 / 凭据 | 仅环境变量 / `.credentials.yaml
|
|
195
|
+
| 配置 / 凭据 | 仅环境变量 / `.credentials.yaml`(无设置页表单) | 设置导航「JIRA 配置」页 + 插件面板卡片 + 同一 `.credentials.yaml` 回退 |
|
|
190
196
|
</details>
|
|
191
197
|
|
|
192
198
|
## License
|
package/lib/client.js
CHANGED
|
@@ -8,6 +8,7 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
var h = React.createElement;
|
|
9
9
|
|
|
10
10
|
var CSS = ".jt-root{box-sizing:border-box;width:100%;padding:2px 0 6px;flex:none;display:flex;flex-direction:column}"
|
|
11
|
+
+ "/* composerStack 是 flex-direction:column:order:99 把面板排到输入卡之后,即输入框下方。 */"
|
|
11
12
|
+ ".jt-hero{order:99;padding:0 var(--dsh-composer-side-clearance, 16px)}"
|
|
12
13
|
+ ".jt-panel{box-sizing:border-box;width:100%;max-width:var(--dsh-composer-card-max-width, 780px);margin:0 auto;border:1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.28));background:var(--dsw-alias-bg-layer-1, rgba(127,127,127,.07));border-radius:10px;overflow:hidden}"
|
|
13
14
|
+ ".jt-header{display:flex;align-items:center;gap:8px;min-height:30px;padding:2px 6px 2px 4px}"
|
|
@@ -52,7 +53,12 @@ window.__ModuleLoader__.load({
|
|
|
52
53
|
+ ".jt-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-label-secondary, #999)}"
|
|
53
54
|
+ ".jt-dot-ok{background:var(--dsw-alias-state-success-primary, #1a9d4f)}"
|
|
54
55
|
+ ".jt-dot-fail{background:var(--dsw-alias-state-error-primary, #d93026)}"
|
|
55
|
-
+ ".jt-dot-testing{background:var(--dsw-alias-state-warn-primary, #b76e00)}"
|
|
56
|
+
+ ".jt-dot-testing{background:var(--dsw-alias-state-warn-primary, #b76e00)}"
|
|
57
|
+
// 设置对话框左侧导航的整页形态(settings.section):
|
|
58
|
+
+ ".jt-set-page{box-sizing:border-box;width:100%;max-width:760px;display:flex;flex-direction:column;gap:12px}"
|
|
59
|
+
+ ".jt-page-title{margin:0;font-size:18px;font-weight:600;line-height:26px;color:var(--dsw-alias-label-primary, #222)}"
|
|
60
|
+
+ ".jt-page-intro{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary, #666)}"
|
|
61
|
+
+ ".jt-set-form{display:flex;flex-direction:column}";
|
|
56
62
|
|
|
57
63
|
var STORAGE_KEY = "dsh.jiraTasks.config.v1";
|
|
58
64
|
|
|
@@ -95,15 +101,6 @@ window.__ModuleLoader__.load({
|
|
|
95
101
|
persistAll(all);
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
// 新版 DSH:SessionSnapshot.blank 是“会话日志为空”的规范标记;旧版 DSH:消息位于 chat.timeline / chat.legacy.nodes。
|
|
99
|
-
function hasMessages(session) {
|
|
100
|
-
if (!session) return false;
|
|
101
|
-
if (typeof session.blank === "boolean") return !session.blank;
|
|
102
|
-
if (session.chat && session.chat.timeline && session.chat.timeline.length > 0) return true;
|
|
103
|
-
if (session.chat && session.chat.legacy && session.chat.legacy.nodes && session.chat.legacy.nodes.length > 0) return true;
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
104
|
function query(projectKey, jql) {
|
|
108
105
|
return fetch("/jira/api/search", {
|
|
109
106
|
method: "POST",
|
|
@@ -122,7 +119,6 @@ window.__ModuleLoader__.load({
|
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
function JiraTasksDock(props) {
|
|
125
|
-
var blankOnly = !!(props && props.blankOnly === true);
|
|
126
122
|
var useWorkspaces = props && typeof props.useWorkspaces === "function" ? props.useWorkspaces : null;
|
|
127
123
|
var sessionId = props && props.sessionId;
|
|
128
124
|
var wsState = useWorkspaces ? useWorkspaces(function (s) { return s; }) : null;
|
|
@@ -173,8 +169,9 @@ window.__ModuleLoader__.load({
|
|
|
173
169
|
return function () { cancelled = true; };
|
|
174
170
|
}, [config, tick]);
|
|
175
171
|
|
|
176
|
-
|
|
177
|
-
|
|
172
|
+
// 面板只注册 conversation.input.dock 一处(见 apply 里的说明):
|
|
173
|
+
// 该槽位在“新会话”和“活跃会话”下都会渲染,且是整行纵向排布的元素,
|
|
174
|
+
// 因此这里没有“仅空白会话显示”的分支,也不需要去重。
|
|
178
175
|
function openEditor() {
|
|
179
176
|
setDraftKey(config.projectKey);
|
|
180
177
|
setDraftJql(config.jql || defaultJql(config.projectKey));
|
|
@@ -187,10 +184,8 @@ window.__ModuleLoader__.load({
|
|
|
187
184
|
setEditing(false);
|
|
188
185
|
}
|
|
189
186
|
|
|
190
|
-
var heroLayout = blankOnly;
|
|
191
|
-
|
|
192
187
|
if (editing) {
|
|
193
|
-
return h("div", { className:
|
|
188
|
+
return h("div", { className: "jt-root jt-hero" },
|
|
194
189
|
h("div", { className: "jt-panel" },
|
|
195
190
|
h("div", { className: "jt-header" }, h("span", { className: "jt-title" }, "JIRA 任务设置")),
|
|
196
191
|
h("div", { className: "jt-body" },
|
|
@@ -260,7 +255,7 @@ window.__ModuleLoader__.load({
|
|
|
260
255
|
body = h("div", { className: "jt-hint" }, "未设置 JIRA 项目 Key,", h("button", { className: "jt-linklike", onClick: openEditor }, "点击配置"));
|
|
261
256
|
}
|
|
262
257
|
|
|
263
|
-
return h("div", { className:
|
|
258
|
+
return h("div", { className: "jt-root jt-hero" },
|
|
264
259
|
h("div", { className: "jt-panel" },
|
|
265
260
|
h("div", { className: "jt-header" },
|
|
266
261
|
h("button", {
|
|
@@ -317,7 +312,9 @@ window.__ModuleLoader__.load({
|
|
|
317
312
|
return snapshot;
|
|
318
313
|
}
|
|
319
314
|
|
|
320
|
-
|
|
315
|
+
// 返回两处渲染位的组件:{ card } 给 plugins.item,{ section } 给 settings.section。
|
|
316
|
+
// 两者共用同一个 JiraSettingsPage 与同一份 scope / credentials 句柄。
|
|
317
|
+
function createJiraSettingsSurfaces(scope, credentials) {
|
|
321
318
|
function readTokenInfo() {
|
|
322
319
|
return credentials.describe(TOKEN_REFS).then(function (res) {
|
|
323
320
|
if (!res || !res.ok || !res.value) return { configured: false, writable: true };
|
|
@@ -347,7 +344,15 @@ window.__ModuleLoader__.load({
|
|
|
347
344
|
});
|
|
348
345
|
}
|
|
349
346
|
|
|
350
|
-
|
|
347
|
+
// 同一个表单服务两处渲染位:
|
|
348
|
+
// - plugins.item:列表里的卡片摘要(view="summary")与点开后的详情(其余 view);
|
|
349
|
+
// - settings.section:设置导航里的整页(variant="page")。
|
|
350
|
+
// 分发组件自身不调用任何 hook,钩子全部留在 JiraSettingsPage 里,这样同一个实例
|
|
351
|
+
// 的 hook 数量与 view 取值无关——这是 React 的硬性要求,早期版本把摘要提前 return
|
|
352
|
+
// 写在 hook 之前会踩这条规则。
|
|
353
|
+
function JiraSettingsPage(props) {
|
|
354
|
+
// "page" = 设置对话框左侧导航的整页(settings.section);默认 = 插件面板里的卡片。
|
|
355
|
+
var asPage = !!(props && props.variant === "page");
|
|
351
356
|
var snapshot = useScopeSnapshot(scope);
|
|
352
357
|
var tokenInfoState = React.useState(null);
|
|
353
358
|
var tokenInfo = tokenInfoState[0], setTokenInfo = tokenInfoState[1];
|
|
@@ -400,7 +405,11 @@ window.__ModuleLoader__.load({
|
|
|
400
405
|
setFailed("");
|
|
401
406
|
var tasks = [];
|
|
402
407
|
if (baseDirty) {
|
|
403
|
-
|
|
408
|
+
// ConfigForm.set/unset 解析为 boolean:false = 宿主拒绝写入或连接处于
|
|
409
|
+
// memory 模式。以前忽略返回值会让面板“看起来保存了”,实际没落盘。
|
|
410
|
+
tasks.push((baseEdit === "" ? scope.unset("baseUrl") : scope.set("baseUrl", baseEdit)).then(function (accepted) {
|
|
411
|
+
if (accepted === false) throw new Error("JIRA 地址保存被宿主拒绝(该命名空间只读,或连接处于 memory 模式)");
|
|
412
|
+
}));
|
|
404
413
|
}
|
|
405
414
|
if (tokenDirty) {
|
|
406
415
|
tasks.push(credentials.set(TOKEN_REF, tokenDraftText).then(function (res) {
|
|
@@ -481,11 +490,9 @@ window.__ModuleLoader__.load({
|
|
|
481
490
|
: probe.phase === "fail" ? "jt-dot jt-dot-fail"
|
|
482
491
|
: probe.phase === "testing" ? "jt-dot jt-dot-testing" : "jt-dot";
|
|
483
492
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
h("span", { className: "jt-set-desc" }, "JIRA 地址与访问令牌。项目 Key / JQL 仍按工作区在会话面板的 ⚙ 中配置。")
|
|
488
|
-
),
|
|
493
|
+
// 字段区在两种形态下完全一致,只有外层容器不同:插件面板卡片(li.jt-set-card)
|
|
494
|
+
// vs 设置导航整页(div.jt-set-page)。h.apply 用来展开字段数组,保持 ES5 风格。
|
|
495
|
+
var fields = [
|
|
489
496
|
h("div", { className: "jt-field" },
|
|
490
497
|
h("label", { htmlFor: "jira-tasks-base-url" }, "JIRA 地址"),
|
|
491
498
|
h("input", {
|
|
@@ -533,48 +540,94 @@ window.__ModuleLoader__.load({
|
|
|
533
540
|
h("button", { className: "jt-btn", disabled: !dirty || busy, onClick: discard }, "放弃"),
|
|
534
541
|
h("button", { className: "jt-btn jt-btn-primary", disabled: !dirty || busy || (baseDirty && !writable) || (tokenDirty && !tokenWritable), onClick: save }, busy ? "保存中…" : "保存")
|
|
535
542
|
)
|
|
536
|
-
|
|
537
|
-
|
|
543
|
+
];
|
|
544
|
+
|
|
545
|
+
if (asPage) {
|
|
546
|
+
return h("div", { className: "jt-set-page" },
|
|
547
|
+
h("h2", { className: "jt-page-title" }, "JIRA 配置"),
|
|
548
|
+
h("p", { className: "jt-page-intro" }, "JIRA 地址与访问令牌(令牌写入凭据存储,不回传前端)。项目 Key / JQL 仍按工作区在会话面板的 ⚙ 中配置。"),
|
|
549
|
+
h.apply(null, ["div", { className: "jt-set-form" }].concat(fields))
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return h.apply(null, ["li", { className: "jt-set-card" },
|
|
554
|
+
h("div", { className: "jt-set-head" },
|
|
555
|
+
h("span", { className: "jt-set-title" }, "JIRA"),
|
|
556
|
+
h("span", { className: "jt-set-desc" }, "JIRA 地址与访问令牌。项目 Key / JQL 仍按工作区在会话面板的 ⚙ 中配置。")
|
|
557
|
+
)
|
|
558
|
+
].concat(fields));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 槽位组件:摘要位只回一行文字,详情位挂载真正的表单(不调用任何 hook)。
|
|
562
|
+
function JiraSettingsCard(props) {
|
|
563
|
+
if (props && props.view === "summary") return "JIRA 地址、令牌与连接状态";
|
|
564
|
+
return h(JiraSettingsPage, null);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// 设置对话框左侧导航的一项(settings.section):整页形态的同一个表单。
|
|
568
|
+
function JiraSettingsSection() {
|
|
569
|
+
return h(JiraSettingsPage, { variant: "page" });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return { card: JiraSettingsCard, section: JiraSettingsSection };
|
|
538
573
|
}
|
|
539
574
|
|
|
540
575
|
function apply(ctx) {
|
|
541
576
|
// ctx.effect(cb) 会立即执行 cb 并把其返回值作为卸载时的清理函数,
|
|
542
577
|
// 所以样式注入必须放在回调内、返回移除函数,否则标签刚插入就被删除。
|
|
543
|
-
|
|
544
|
-
ctx.effect(function () {
|
|
545
|
-
var styleTag = document.createElement("style");
|
|
546
|
-
styleTag.textContent = CSS;
|
|
547
|
-
document.head.appendChild(styleTag);
|
|
548
|
-
return function () { try { styleTag.remove(); } catch (e) {} };
|
|
549
|
-
});
|
|
550
|
-
} else {
|
|
578
|
+
ctx.effect(function () {
|
|
551
579
|
var styleTag = document.createElement("style");
|
|
552
580
|
styleTag.textContent = CSS;
|
|
553
581
|
document.head.appendChild(styleTag);
|
|
554
|
-
|
|
582
|
+
return function () { try { styleTag.remove(); } catch (e) {} };
|
|
583
|
+
}, "jira-tasks: styles");
|
|
555
584
|
var slots = ctx.get("slots");
|
|
556
585
|
if (!slots) return;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
586
|
+
// 只注册 conversation.input.dock 一处。
|
|
587
|
+
//
|
|
588
|
+
// 为什么不用 conversation.composer.dock:该槽位在 DSH 0.1.7 里渲染进
|
|
589
|
+
// InputBar 的 .dock 行——一个 justify-content:center 的**横向** flex 行,
|
|
590
|
+
// 与右侧的上下文占用环(ContextMeter)并排。整宽任务面板放进那一行,会和
|
|
591
|
+
// 占用环抢同一条 flex line,面板右侧内容被环压住。
|
|
592
|
+
//
|
|
593
|
+
// conversation.input.dock 属于 composerStack(flex-direction:column),
|
|
594
|
+
// 在新会话与活跃会话下都会渲染(只要有 session + input),是整宽纵向行;
|
|
595
|
+
// 面板自身用 CSS order:99 排到输入卡之后,即“输入框下方”。
|
|
560
596
|
slots.inject("conversation.input.dock", function () {
|
|
561
|
-
return slots.register({ name: "conversation.input.dock", id: "jira-open-tasks
|
|
562
|
-
return h(JiraTasksDock, Object.assign({}, props, { blankOnly: true }));
|
|
563
|
-
});
|
|
597
|
+
return slots.register({ name: "conversation.input.dock", id: "jira-open-tasks", order: 10 }, JiraTasksDock);
|
|
564
598
|
});
|
|
565
599
|
|
|
566
|
-
//
|
|
567
|
-
|
|
600
|
+
// 设置表单的两处注册。DSH 0.1.5+ :设置表单的服务是 configForms,命名空间就是
|
|
601
|
+
// 本插件的 profile 条目 id(jira-tasks);两处都只在宿主确实提供了该命名空间
|
|
602
|
+
// 时才出现(whileServed 的 disposer 交回各自的 effect)。
|
|
603
|
+
//
|
|
604
|
+
// 1) 插件面板卡片(plugins.item):插件自己的配置项,原来就在这里。
|
|
605
|
+
// 2) 设置对话框左侧导航项(settings.section):id/order/label 是导航身份,
|
|
606
|
+
// order 大于 Agent 预设(20),所以排在它下面;label 直接给字符串
|
|
607
|
+
// (槽位用 resolveSlotLabel 读,字符串与 thunk 都接受)。未知 id 由设置
|
|
608
|
+
// 外壳回退到默认齿轮图标,无需注册图标。
|
|
609
|
+
var configForms = ctx.get("configForms");
|
|
568
610
|
var remote = ctx.get("remote");
|
|
569
|
-
if (
|
|
570
|
-
var
|
|
571
|
-
|
|
572
|
-
return
|
|
573
|
-
|
|
611
|
+
if (configForms && remote && remote.credentials) {
|
|
612
|
+
var surfaces = createJiraSettingsSurfaces(configForms.get(SETTINGS_NS), remote.credentials);
|
|
613
|
+
ctx.effect(function () {
|
|
614
|
+
return configForms.whileServed([SETTINGS_NS], function () {
|
|
615
|
+
return slots.inject("plugins.item", function () {
|
|
616
|
+
return slots.register({ name: "plugins.item", id: SETTINGS_NS, order: 41, label: "JIRA" }, surfaces.card);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
}, "jira-tasks: plugins page card");
|
|
620
|
+
ctx.effect(function () {
|
|
621
|
+
return configForms.whileServed([SETTINGS_NS], function () {
|
|
622
|
+
return slots.inject("settings.section", function () {
|
|
623
|
+
return slots.register({ name: "settings.section", id: SETTINGS_NS, order: 30, label: "JIRA 配置" }, surfaces.section);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
}, "jira-tasks: settings section");
|
|
574
627
|
}
|
|
575
628
|
}
|
|
576
629
|
|
|
577
|
-
exports.inject = ["slots", "
|
|
630
|
+
exports.inject = ["slots", "configForms", "remote", "remote.credentials"];
|
|
578
631
|
exports.apply = apply;
|
|
579
632
|
return module.exports;
|
|
580
633
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-jira-tasks — HOST half (persistent profile plugin).
|
|
3
|
-
* Serves POST /jira/api/search: reads the JIRA base URL from
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* issue JSON.
|
|
3
|
+
* Serves POST /jira/api/search: reads the JIRA base URL from this entry's own
|
|
4
|
+
* Config (the `jira-tasks` profile entry, editable on the Plugins settings
|
|
5
|
+
* page) and the token from the credentials service — the settings-page
|
|
6
|
+
* JIRA_TASKS_TOKEN first, so it overrides an exported JIRA_API_TOKEN /
|
|
7
|
+
* JIRA_TOKEN — then queries JIRA /rest/api/2/search through subprocess+curl
|
|
8
|
+
* (auth header via stdin --config -), returning normalized issue JSON.
|
|
9
9
|
*/
|
|
10
10
|
import z from '@deepseek-ai/schemastery';
|
|
11
11
|
|
|
12
12
|
/** Settings namespace exposing the editable connection fields in the GUI. */
|
|
13
13
|
const SETTINGS_NS = 'jira-tasks';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Diagnostics name for this plugin instance. Deliberately the same string as
|
|
17
|
+
* {@link SETTINGS_NS} (= the profile entry id, `jira-tasks`): that id — not this
|
|
18
|
+
* name and not the package name `dsh-jira-tasks` — is the settings namespace the
|
|
19
|
+
* client card edits, so deriving one from the other removes a whole class of
|
|
20
|
+
* confusion when reading loader logs.
|
|
21
|
+
*/
|
|
22
|
+
const name = SETTINGS_NS;
|
|
23
|
+
|
|
15
24
|
/**
|
|
16
25
|
* Token references in precedence order. `JIRA_TASKS_TOKEN` is the plugin-owned
|
|
17
26
|
* ref the settings card writes to: unlike `JIRA_API_TOKEN`, the credentials
|
|
@@ -22,17 +31,41 @@ const SETTINGS_NS = 'jira-tasks';
|
|
|
22
31
|
const TOKEN_REFS = ['JIRA_TASKS_TOKEN', 'JIRA_API_TOKEN', 'JIRA_TOKEN'];
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
|
-
* Durable connection schema.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
34
|
+
* Durable connection schema. Since DSH 0.1.5 the settings service no longer
|
|
35
|
+
* takes a `register(ns, schema)` call: a plugin's OWN Config IS the form its
|
|
36
|
+
* settings page is built from, keyed by the profile entry id (`jira-tasks`).
|
|
37
|
+
* `baseUrl` is volatile, so a saved change reaches the running plugin without
|
|
38
|
+
* remounting it. The token is deliberately NOT here — it stays in the
|
|
39
|
+
* credentials store (write-only from the client), so no secret ever lands in
|
|
40
|
+
* the profile configuration document.
|
|
28
41
|
*/
|
|
29
|
-
const
|
|
30
|
-
baseUrl: z.string().default('').description('JIRA 地址,例如 http://jira.example.com/')
|
|
42
|
+
const Config = z.object({
|
|
43
|
+
baseUrl: z.string().default('').volatile().description('JIRA 地址,例如 http://jira.example.com/')
|
|
31
44
|
});
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
const inject = ['subprocess', 'credentials', 'sandboxPolicy', 'webServer'];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Answer a non-POST request with 405. `webServer.register` dispatches on the
|
|
50
|
+
* pathname only, so without this guard a stray GET would run a real JIRA
|
|
51
|
+
* query. Returns true when the caller may handle the request.
|
|
52
|
+
* @param req - the HTTP request.
|
|
53
|
+
* @param res - the response the guard writes to on refusal.
|
|
54
|
+
* @returns true for POST, false after answering 405.
|
|
55
|
+
*/
|
|
56
|
+
function methodIsPost(req, res) {
|
|
57
|
+
if (req.method === 'POST') return true;
|
|
58
|
+
res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store', Allow: 'POST' });
|
|
59
|
+
res.end('method not allowed');
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Mount the host half: the /jira/api/* routes and the settings-page policy.
|
|
65
|
+
* @param ctx - plugin context.
|
|
66
|
+
* @param config - this entry's live Config (profile entry `jira-tasks`).
|
|
67
|
+
*/
|
|
68
|
+
function apply(ctx, config) {
|
|
36
69
|
const sendJson = (res, obj) => {
|
|
37
70
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
38
71
|
res.end(JSON.stringify(obj));
|
|
@@ -65,19 +98,32 @@ export default {
|
|
|
65
98
|
return undefined;
|
|
66
99
|
}
|
|
67
100
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
101
|
+
// DSH 0.1.5+ :设置页的表单直接来自本条目自己的 Config,settings 服务不再有
|
|
102
|
+
// register(ns, schema)。这里把本实例的“自动生成页”关掉,改由客户端注册的
|
|
103
|
+
// 自定义卡片(地址 + 令牌 + 连通性测试)接管。
|
|
104
|
+
// 两个易错点:1) configure 的第二个参数必须是**本插件实例**的 fiber——默认值是
|
|
105
|
+
// settings 服务自己的 fiber,而 describe() 是按 entry.fiber 查策略的,传错不生效;
|
|
106
|
+
// 2) 返回值是注销函数,必须交回 effect,否则卸载后策略会一直挂在死 fiber 上。
|
|
70
107
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
71
|
-
settingsCtx.
|
|
108
|
+
settingsCtx.effect(() => {
|
|
109
|
+
try {
|
|
110
|
+
return settingsCtx.settings.configure({ auto: false }, ctx.fiber);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// 同一实例重复配置等情况下不阻断插件加载,但要留下可诊断的日志。
|
|
113
|
+
if (ctx.logger && typeof ctx.logger.warn === 'function') {
|
|
114
|
+
ctx.logger.warn('jira-tasks: settings page policy not applied: %s', (e && e.message) || e);
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}, 'jira-tasks: settings page policy');
|
|
72
119
|
});
|
|
73
120
|
|
|
74
|
-
/**
|
|
121
|
+
/** 本条目 Config 里的 JIRA 地址(volatile 引用),未配置时返回 ''。 */
|
|
75
122
|
function settingsBaseUrl() {
|
|
76
123
|
try {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return section && typeof section.baseUrl === 'string' ? section.baseUrl.trim() : '';
|
|
124
|
+
const ref = config && config.baseUrl;
|
|
125
|
+
const value = ref && typeof ref.get === 'function' ? ref.get() : ref;
|
|
126
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
81
127
|
} catch (e) {
|
|
82
128
|
return '';
|
|
83
129
|
}
|
|
@@ -254,28 +300,33 @@ export default {
|
|
|
254
300
|
}
|
|
255
301
|
|
|
256
302
|
if (webServer && typeof webServer.register === 'function') {
|
|
257
|
-
|
|
303
|
+
// 每条路由都必须挂在自己的 effect 上:register 返回注销函数,卸载/重载时先释放,
|
|
304
|
+
// 否则重挂载会因为“同一 (kind, path) 只允许一条路由”直接抛错,整个插件激活失败。
|
|
305
|
+
ctx.effect(() => webServer.register({
|
|
258
306
|
kind: 'exact',
|
|
259
307
|
path: '/jira/api/search',
|
|
260
308
|
handler: async (req, res) => {
|
|
309
|
+
if (!methodIsPost(req, res)) return;
|
|
261
310
|
try {
|
|
262
311
|
sendJson(res, await handleSearch(await readBody(req)));
|
|
263
312
|
} catch (e) {
|
|
264
313
|
sendJson(res, { ok: false, error: String((e && e.message) || e) });
|
|
265
314
|
}
|
|
266
315
|
}
|
|
267
|
-
});
|
|
268
|
-
webServer.register({
|
|
316
|
+
}), 'jira-tasks: /jira/api/search');
|
|
317
|
+
ctx.effect(() => webServer.register({
|
|
269
318
|
kind: 'exact',
|
|
270
319
|
path: '/jira/api/test',
|
|
271
320
|
handler: async (req, res) => {
|
|
321
|
+
if (!methodIsPost(req, res)) return;
|
|
272
322
|
try {
|
|
273
323
|
sendJson(res, await handleTest(await readBody(req)));
|
|
274
324
|
} catch (e) {
|
|
275
325
|
sendJson(res, { ok: false, code: 'error', error: String((e && e.message) || e) });
|
|
276
326
|
}
|
|
277
327
|
}
|
|
278
|
-
});
|
|
328
|
+
}), 'jira-tasks: /jira/api/test');
|
|
279
329
|
}
|
|
280
|
-
|
|
281
|
-
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export { Config, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-jira-tasks",
|
|
3
|
-
"version": "1.0
|
|
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.",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "JIRA open tasks panel for DeepSeek Harness (DSH) 0.1.7: 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",
|
|
7
7
|
"exports": {
|
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
"platform": "web",
|
|
41
41
|
"inject": [
|
|
42
42
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-workspace",
|
|
43
45
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
44
47
|
"@deepseek-ai/dsh-api-remotes"
|
|
45
48
|
]
|
|
46
49
|
}
|