dsh-jira-tasks 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 liu3734
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # dsh-jira-tasks
2
+
3
+ JIRA 开启任务面板 —— DeepSeek Harness(DSH)持久化插件(profile bundle)。
4
+
5
+ 在 DSH 会话**输入框下方**展示当前 JIRA 项目**指派给当前用户**的「开启 / 重新开启」任务列表。项目 Key 与 JQL **按工作区配置**并持久化,重启 DSH 不丢失。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ dsh plugin --profile desktop add dsh-jira-tasks
11
+ # 或手动安装:见仓库 README「注册方式二:正式安装」
12
+ ```
13
+
14
+ 重启 DSH 后生效。
15
+
16
+ ## 配置
17
+
18
+ 1. **JIRA 地址与令牌**:写入 `$DSH_HOME/.credentials.yaml`(或导出环境变量):
19
+
20
+ ```yaml
21
+ JIRA_BASE_URL: "http://jira.example.com/"
22
+ JIRA_API_TOKEN: "<PAT 或 user:token>"
23
+ ```
24
+
25
+ 2. **项目 Key / JQL**:点击面板 ⚙ 按工作区配置;JQL 留空使用默认查询:
26
+
27
+ ```jql
28
+ project = "{projectKey}" AND status in ("开启", "重新开启") AND assignee = currentUser() ORDER BY updated DESC
29
+ ```
30
+
31
+ ## 架构
32
+
33
+ - **Host**:`webServer` 路由 `POST /jira/api/search`,凭据走 `credentials` 服务,查询经 `subprocess` + curl(认证头经 stdin `--config -` 传入)。
34
+ - **Client**:`window.__ModuleLoader__.load({ id, factory })` web bundle,注册 `conversation.composer.dock`(活跃会话)与 `conversation.input.dock`(新会话,`order:99` 置于输入框下方、等宽)。
35
+
36
+ ## 卸载
37
+
38
+ ```bash
39
+ dsh plugin --profile desktop remove dsh-jira-tasks
40
+ ```
41
+
42
+ ## License
43
+
44
+ MIT
@@ -0,0 +1,11 @@
1
+ # dsh-jira-tasks — bundle patch layer.
2
+ # Mounts the host half into the profile composition. The inject list makes the
3
+ # loader wait until these services are ready before activating the plugin.
4
+ - insert:
5
+ - id: jira-tasks
6
+ name: 'dsh-jira-tasks'
7
+ inject:
8
+ - subprocess
9
+ - credentials
10
+ - sandboxPolicy
11
+ - webServer
package/lib/client.js ADDED
@@ -0,0 +1,297 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-jira-tasks",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ var React = require("react");
8
+ var h = React.createElement;
9
+
10
+ var CSS = ".jt-root{box-sizing:border-box;width:100%;padding:2px 8px 6px;flex:none;display:flex;flex-direction:column}"
11
+ + ".jt-hero{order:99;padding:0 var(--dsh-composer-side-clearance, 16px)}"
12
+ + ".jt-hero .jt-panel{width:100%;max-width:var(--dsh-composer-card-max-width, 780px);margin:0 auto;box-sizing:border-box}"
13
+ + ".jt-panel{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}"
14
+ + ".jt-header{display:flex;align-items:center;gap:8px;min-height:30px;padding:2px 6px 2px 4px}"
15
+ + ".jt-toggle{display:flex;align-items:center;gap:6px;flex:1;min-width:0;background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-primary, #222);padding:4px 6px;border-radius:6px;font-size:12px;text-align:left}"
16
+ + ".jt-toggle:hover{background:var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.1))}"
17
+ + ".jt-chevron{flex:none;font-size:10px;color:var(--dsw-alias-label-secondary, #666)}"
18
+ + ".jt-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}"
19
+ + ".jt-badge{flex:none;font-size:11px;padding:0 6px;border-radius:999px;background:var(--dsw-alias-button-primary-fill, #2f6fed);color:var(--dsw-alias-label-primary-foreground, #fff)}"
20
+ + ".jt-actions{display:flex;align-items:center;gap:2px;flex:none}"
21
+ + ".jt-action{width:24px;height:24px;border:none;background:none;cursor:pointer;border-radius:6px;color:var(--dsw-alias-label-secondary, #666);font-size:13px;line-height:1}"
22
+ + ".jt-action:hover{background:var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.14))}"
23
+ + ".jt-body{padding:0 8px 8px}"
24
+ + ".jt-hint{color:var(--dsw-alias-label-secondary, #666);font-size:12px;line-height:1.7}"
25
+ + ".jt-error{color:var(--dsw-alias-state-error-primary, #d93026);font-size:12px;line-height:1.7;word-break:break-word}"
26
+ + ".jt-empty{color:var(--dsw-alias-label-secondary, #666);font-size:12px;padding:4px 0}"
27
+ + ".jt-list{margin:0;padding:2px 0;list-style:none;max-height:190px;overflow-y:auto;display:flex;flex-direction:column;gap:2px}"
28
+ + ".jt-item{margin:0;padding:0;list-style:none}"
29
+ + ".jt-row{display:flex;align-items:baseline;gap:8px;padding:4px 6px;border-radius:6px;text-decoration:none;color:var(--dsw-alias-label-primary, #222)}"
30
+ + ".jt-row:hover{background:var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.09))}"
31
+ + ".jt-key{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--dsw-alias-brand-primary, #2f6fed);font-weight:600}"
32
+ + ".jt-summary{flex:1;min-width:0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}"
33
+ + ".jt-meta{flex:none;display:flex;align-items:center;gap:6px;font-size:11px;color:var(--dsw-alias-label-secondary, #666)}"
34
+ + ".jt-chip{padding:0 6px;border-radius:999px;font-size:11px;line-height:16px;white-space:nowrap;color:var(--dsw-alias-label-secondary, #888);background:color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 12%, transparent)}"
35
+ + ".jt-chip-new{color:var(--dsw-alias-state-warn-primary, #b76e00);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary, #b76e00) 14%, transparent)}"
36
+ + ".jt-chip-progress{color:var(--dsw-alias-brand-primary, #2f6fed);background:color-mix(in srgb, var(--dsw-alias-brand-primary, #2f6fed) 12%, transparent)}"
37
+ + ".jt-field{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}"
38
+ + ".jt-field label{font-size:12px;color:var(--dsw-alias-label-secondary, #666)}"
39
+ + ".jt-input{box-sizing:border-box;width:100%;padding:5px 8px;font-size:12px;font-family:inherit;border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.32));border-radius:6px;background:var(--dsw-alias-bg-base, #fff);color:var(--dsw-alias-label-primary, #222);outline:none}"
40
+ + ".jt-input:focus{border-color:var(--dsw-alias-brand-primary, #2f6fed)}"
41
+ + ".jt-textarea{min-height:64px;resize:vertical;line-height:1.5}"
42
+ + ".jt-buttons{display:flex;gap:8px;justify-content:flex-end}"
43
+ + ".jt-btn{padding:4px 12px;font-size:12px;border-radius:6px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.32));background:transparent;color:var(--dsw-alias-label-primary, #222)}"
44
+ + ".jt-btn:hover{background:var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.1));opacity:1}"
45
+ + ".jt-btn-primary{background:var(--dsw-alias-button-primary-fill, #2f6fed);border-color:transparent;color:var(--dsw-alias-label-primary-foreground, #fff)}"
46
+ + ".jt-btn-primary:hover{background:var(--dsw-alias-button-primary-hover, #3a7bfd)}"
47
+ + ".jt-linklike{background:none;border:none;padding:0;cursor:pointer;color:var(--dsw-alias-brand-primary, #2f6fed);font-size:12px;text-decoration:underline}";
48
+
49
+ var STORAGE_KEY = "dsh.jiraTasks.config.v1";
50
+
51
+ function defaultJql(key) {
52
+ return 'project = "' + (key || "{projectKey}") + '" AND status in ("开启", "重新开启") AND assignee = currentUser() ORDER BY updated DESC';
53
+ }
54
+
55
+ function loadAll() {
56
+ try {
57
+ if (typeof window === "undefined" || typeof window.localStorage === "undefined") return null;
58
+ var raw = window.localStorage.getItem(STORAGE_KEY);
59
+ if (!raw) return null;
60
+ return JSON.parse(raw);
61
+ } catch (e) { return null; }
62
+ }
63
+ function persistAll(all) {
64
+ try {
65
+ if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") window.localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
66
+ } catch (e) {}
67
+ }
68
+ function isLegacy(all) {
69
+ return all !== null && typeof all === "object" && (typeof all.projectKey === "string" || typeof all.jql === "string");
70
+ }
71
+ function loadConfigFor(wsKey) {
72
+ var all = loadAll();
73
+ if (all === null) return { projectKey: "", jql: "" };
74
+ if (isLegacy(all)) {
75
+ var legacy = { projectKey: String(all.projectKey || ""), jql: String(all.jql || "") };
76
+ if (wsKey) { var mig = {}; mig[wsKey] = legacy; persistAll(mig); return legacy; }
77
+ return { projectKey: "", jql: "" };
78
+ }
79
+ var entry = all[wsKey];
80
+ if (!entry) return { projectKey: "", jql: "" };
81
+ return { projectKey: String(entry.projectKey || ""), jql: String(entry.jql || "") };
82
+ }
83
+ function saveConfigFor(wsKey, config) {
84
+ var all = loadAll();
85
+ if (all === null || isLegacy(all)) all = {};
86
+ all[wsKey] = { projectKey: config.projectKey, jql: config.jql };
87
+ persistAll(all);
88
+ }
89
+
90
+ function hasMessages(session) {
91
+ if (!session || !session.chat) return false;
92
+ if (session.chat.timeline && session.chat.timeline.length > 0) return true;
93
+ if (session.chat.legacy && session.chat.legacy.nodes && session.chat.legacy.nodes.length > 0) return true;
94
+ return false;
95
+ }
96
+
97
+ function query(projectKey, jql) {
98
+ return fetch("/jira/api/search", {
99
+ method: "POST",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify({ projectKey: projectKey, jql: jql })
102
+ }).then(function (r) { return r.json(); });
103
+ }
104
+
105
+ function JiraTasksDock(props) {
106
+ var blankOnly = !!(props && props.blankOnly === true);
107
+ var useWorkspaces = props && typeof props.useWorkspaces === "function" ? props.useWorkspaces : null;
108
+ var sessionId = props && props.sessionId;
109
+ var wsState = useWorkspaces ? useWorkspaces(function (s) { return s; }) : null;
110
+ var wsKey = "";
111
+ var wsTitle = "";
112
+ try {
113
+ if (sessionId && wsState && wsState.items && Array.isArray(wsState.items)) {
114
+ var item = wsState.items.find(function (w) { return w.sessionIds && w.sessionIds.indexOf(sessionId) !== -1; });
115
+ if (item) { wsKey = String(item.path || item.workspaceId || ""); wsTitle = String(item.title || ""); }
116
+ }
117
+ } catch (e) {}
118
+
119
+ var configState = React.useState(function () { return loadConfigFor(wsKey); });
120
+ var config = configState[0], setConfig = configState[1];
121
+ var phaseState = React.useState("idle");
122
+ var phase = phaseState[0], setPhase = phaseState[1];
123
+ var resultState = React.useState(null);
124
+ var result = resultState[0], setResult = resultState[1];
125
+ var tickState = React.useState(0);
126
+ var tick = tickState[0], setTick = tickState[1];
127
+ var editingState = React.useState(false);
128
+ var editing = editingState[0], setEditing = editingState[1];
129
+ var collapsedState = React.useState(false);
130
+ var collapsed = collapsedState[0], setCollapsed = collapsedState[1];
131
+ var draftKeyState = React.useState("");
132
+ var draftKey = draftKeyState[0], setDraftKey = draftKeyState[1];
133
+ var draftJqlState = React.useState("");
134
+ var draftJql = draftJqlState[0], setDraftJql = draftJqlState[1];
135
+
136
+ React.useEffect(function () { setConfig(loadConfigFor(wsKey)); }, [wsKey]);
137
+
138
+ React.useEffect(function () {
139
+ var key = (config.projectKey || "").trim();
140
+ if (!key) { setPhase("idle"); setResult(null); return; }
141
+ var cancelled = false;
142
+ setPhase("loading");
143
+ query(key, (config.jql || "").trim())
144
+ .then(function (res) {
145
+ if (cancelled) return;
146
+ setResult(res || null);
147
+ setPhase(res && res.ok ? "ready" : "error");
148
+ })
149
+ .catch(function (err) {
150
+ if (cancelled) return;
151
+ setResult({ ok: false, error: String((err && err.message) || err) });
152
+ setPhase("error");
153
+ });
154
+ return function () { cancelled = true; };
155
+ }, [config, tick]);
156
+
157
+ if (blankOnly && hasMessages(props.session)) return null;
158
+
159
+ function openEditor() {
160
+ setDraftKey(config.projectKey);
161
+ setDraftJql(config.jql || defaultJql(config.projectKey));
162
+ setEditing(true);
163
+ }
164
+ function saveEditor() {
165
+ var next = { projectKey: (draftKey || "").trim(), jql: (draftJql || "").trim() };
166
+ saveConfigFor(wsKey, next);
167
+ setConfig(next);
168
+ setEditing(false);
169
+ }
170
+
171
+ var heroLayout = blankOnly;
172
+
173
+ if (editing) {
174
+ return h("div", { className: heroLayout ? "jt-root jt-hero" : "jt-root" },
175
+ h("div", { className: "jt-panel" },
176
+ h("div", { className: "jt-header" }, h("span", { className: "jt-title" }, "JIRA 任务设置")),
177
+ h("div", { className: "jt-body" },
178
+ h("div", { className: "jt-hint", style: { marginBottom: "8px" } }, "配置保存到当前工作区:" + (wsTitle || wsKey || "(无工作区)")),
179
+ h("div", { className: "jt-field" },
180
+ h("label", null, "项目 Key"),
181
+ h("input", {
182
+ className: "jt-input", value: draftKey, placeholder: "例如 ROMS",
183
+ onChange: function (e) {
184
+ var nextKey = e.target.value;
185
+ setDraftKey(nextKey);
186
+ setDraftJql(function (prev) { return prev === defaultJql(draftKey) ? defaultJql(nextKey) : prev; });
187
+ },
188
+ onKeyDown: function (e) { if (e.key === "Enter") saveEditor(); }
189
+ })
190
+ ),
191
+ h("div", { className: "jt-field" },
192
+ h("label", null, "JQL(留空则使用默认查询;{projectKey} 会被替换为项目 Key)"),
193
+ h("textarea", {
194
+ className: "jt-input jt-textarea", value: draftJql, rows: 4, placeholder: defaultJql(""),
195
+ onChange: function (e) { setDraftJql(e.target.value); }
196
+ })
197
+ ),
198
+ h("div", { className: "jt-buttons" },
199
+ h("button", { className: "jt-btn", onClick: function () { setEditing(false); } }, "取消"),
200
+ h("button", { className: "jt-btn jt-btn-primary", onClick: saveEditor }, "保存并查询")
201
+ )
202
+ )
203
+ )
204
+ );
205
+ }
206
+
207
+ var projectKey = (config.projectKey || "").trim();
208
+ var error = result && !result.ok ? result.error : "";
209
+ var count = result && result.ok && typeof result.total === "number" ? result.total : null;
210
+ var baseUrl = result && result.ok ? result.baseUrl : "";
211
+
212
+ var rows = (result && result.ok && Array.isArray(result.issues) ? result.issues : []).map(function (issue) {
213
+ var chipClass = issue.statusCategory === "new" ? "jt-chip jt-chip-new" : (issue.statusCategory === "indeterminate" ? "jt-chip jt-chip-progress" : "jt-chip");
214
+ var meta = [];
215
+ if (issue.status) meta.push(h("span", { className: chipClass }, issue.status));
216
+ if (issue.priority) meta.push(h("span", null, issue.priority));
217
+ if (issue.issueType) meta.push(h("span", null, issue.issueType));
218
+ return h("li", { key: issue.key, className: "jt-item" },
219
+ h("a", {
220
+ className: "jt-row",
221
+ href: baseUrl ? baseUrl + "/browse/" + encodeURIComponent(issue.key) : undefined,
222
+ target: baseUrl ? "_blank" : undefined,
223
+ rel: "noreferrer",
224
+ title: issue.key + " " + issue.summary
225
+ },
226
+ h("span", { className: "jt-key" }, issue.key),
227
+ h("span", { className: "jt-summary" }, issue.summary),
228
+ h("span", { className: "jt-meta" }, meta.length > 0 ? meta : null)
229
+ )
230
+ );
231
+ });
232
+
233
+ var body;
234
+ if (phase === "loading") {
235
+ body = h("div", { className: "jt-hint" }, "正在查询 JIRA 任务…");
236
+ } else if (phase === "error") {
237
+ body = h("div", { className: "jt-error" }, "查询失败:" + error);
238
+ } else if (phase === "ready") {
239
+ body = rows.length > 0 ? h("ul", { className: "jt-list" }, rows) : h("div", { className: "jt-empty" }, "该项目暂无未解决的任务");
240
+ } else {
241
+ body = h("div", { className: "jt-hint" }, "未设置 JIRA 项目 Key,", h("button", { className: "jt-linklike", onClick: openEditor }, "点击配置"));
242
+ }
243
+
244
+ return h("div", { className: heroLayout ? "jt-root jt-hero" : "jt-root" },
245
+ h("div", { className: "jt-panel" },
246
+ h("div", { className: "jt-header" },
247
+ h("button", {
248
+ className: "jt-toggle",
249
+ onClick: function () { setCollapsed(!collapsed); },
250
+ title: collapsed ? "展开" : "收起"
251
+ },
252
+ h("span", { className: "jt-chevron" }, collapsed ? "▸" : "▾"),
253
+ h("span", { className: "jt-title" }, "JIRA · " + (projectKey || "未配置")),
254
+ count !== null ? h("span", { className: "jt-badge" }, String(count)) : null
255
+ ),
256
+ h("div", { className: "jt-actions" },
257
+ h("button", { className: "jt-action", title: "刷新", onClick: function () { setTick(tick + 1); } }, "⟳"),
258
+ h("button", { className: "jt-action", title: "设置项目 Key / JQL", onClick: openEditor }, "⚙")
259
+ )
260
+ ),
261
+ collapsed ? null : h("div", { className: "jt-body" }, body)
262
+ )
263
+ );
264
+ }
265
+
266
+ function apply(ctx) {
267
+ // ctx.effect(cb) 会立即执行 cb 并把其返回值作为卸载时的清理函数,
268
+ // 所以样式注入必须放在回调内、返回移除函数,否则标签刚插入就被删除。
269
+ if (ctx && typeof ctx.effect === "function") {
270
+ ctx.effect(function () {
271
+ var styleTag = document.createElement("style");
272
+ styleTag.textContent = CSS;
273
+ document.head.appendChild(styleTag);
274
+ return function () { try { styleTag.remove(); } catch (e) {} };
275
+ });
276
+ } else {
277
+ var styleTag = document.createElement("style");
278
+ styleTag.textContent = CSS;
279
+ document.head.appendChild(styleTag);
280
+ }
281
+ var slots = ctx.get("slots");
282
+ if (!slots) return;
283
+ slots.inject("conversation.composer.dock", function () {
284
+ return slots.register({ name: "conversation.composer.dock", id: "jira-open-tasks", order: 5 }, JiraTasksDock);
285
+ });
286
+ slots.inject("conversation.input.dock", function () {
287
+ return slots.register({ name: "conversation.input.dock", id: "jira-open-tasks-hero", order: 10 }, function (props) {
288
+ return h(JiraTasksDock, Object.assign({}, props, { blankOnly: true }));
289
+ });
290
+ });
291
+ }
292
+
293
+ exports.inject = ["slots"];
294
+ exports.apply = apply;
295
+ return module.exports;
296
+ }
297
+ });
package/lib/index.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * dsh-jira-tasks — HOST half (persistent profile plugin).
3
+ * Serves POST /jira/api/search: reads JIRA_BASE_URL / JIRA_API_TOKEN via the
4
+ * credentials service, queries JIRA /rest/api/2/search through subprocess+curl
5
+ * (auth header via stdin --config -), returns normalized issue JSON.
6
+ */
7
+ export default {
8
+ inject: ['subprocess', 'credentials', 'sandboxPolicy', 'webServer'],
9
+ apply(ctx) {
10
+ const sendJson = (res, obj) => {
11
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
12
+ res.end(JSON.stringify(obj));
13
+ };
14
+ const readBody = (req) => new Promise((resolve) => {
15
+ const chunks = [];
16
+ req.on('data', (c) => chunks.push(c));
17
+ req.on('end', () => {
18
+ try {
19
+ const text = Buffer.concat(chunks).toString('utf-8') || '{}';
20
+ resolve(JSON.parse(text));
21
+ } catch (e) { resolve({}); }
22
+ });
23
+ req.on('error', () => resolve({}));
24
+ });
25
+
26
+ const subprocess = ctx.get('subprocess');
27
+ const credentials = ctx.get('credentials');
28
+ const sandboxPolicy = ctx.get('sandboxPolicy');
29
+ const webServer = ctx.get('webServer');
30
+ const workspaceRoot = sandboxPolicy && sandboxPolicy.workspaceRoot ? sandboxPolicy.workspaceRoot : '/tmp';
31
+
32
+ async function resolveFirst(names) {
33
+ for (const name of names) {
34
+ try {
35
+ const cred = await credentials.resolve(name);
36
+ if (cred && typeof cred.value === 'string' && cred.value.length > 0) return cred.value;
37
+ } catch (e) { /* try next */ }
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ function buildAuthHeader(token) {
43
+ if (token.indexOf(':') !== -1) return 'Basic ' + Buffer.from(token, 'utf-8').toString('base64');
44
+ return 'Bearer ' + token;
45
+ }
46
+
47
+ function buildJql(template, projectKey) {
48
+ const t = (template || '').trim();
49
+ if (!t) return 'project = "' + projectKey + '" AND status in ("开启", "重新开启") AND assignee = currentUser() ORDER BY updated DESC';
50
+ return t.replace(/\{projectKey\}/g, projectKey).replace(/\{key\}/g, projectKey);
51
+ }
52
+
53
+ async function queryJira(baseUrl, token, jql) {
54
+ const auth = buildAuthHeader(token);
55
+ const cleanBase = baseUrl.replace(/\/+$/, '');
56
+ let curl;
57
+ try {
58
+ curl = await subprocess.resolveExecutable('curl');
59
+ } catch (e) {
60
+ curl = '/usr/bin/curl';
61
+ }
62
+ const handle = subprocess.spawn({
63
+ argv: [
64
+ curl, '-sS', '--fail-with-body', '--max-time', '20', '-G',
65
+ cleanBase + '/rest/api/2/search',
66
+ '--data-urlencode', 'jql=' + jql,
67
+ '--data-urlencode', 'maxResults=50',
68
+ '--data-urlencode', 'fields=summary,status,priority,issuetype,statuscategorykey',
69
+ '-H', 'Accept: application/json',
70
+ '--config', '-'
71
+ ],
72
+ cwd: workspaceRoot,
73
+ stdio: {
74
+ stdin: { data: 'header = "Authorization: ' + auth + '"\n' },
75
+ stdout: { maxBytes: 4 * 1024 * 1024, spill: { maxBytes: 64 * 1024 * 1024 } },
76
+ stderr: { maxBytes: 1024 * 1024, spill: { maxBytes: 16 * 1024 * 1024 } }
77
+ },
78
+ graceMs: 5000
79
+ });
80
+ const outcome = await handle.done;
81
+ const stdout = (handle.collected.stdout ? handle.collected.stdout.readFrom(0).text : '') || '';
82
+ const stderr = (handle.collected.stderr ? handle.collected.stderr.readFrom(0).text : '') || '';
83
+ return { exitCode: outcome.exitCode, stdout, stderr };
84
+ }
85
+
86
+ async function handleSearch(args) {
87
+ const projectKey = String((args && args.projectKey) || '').trim();
88
+ if (!projectKey) return { ok: false, error: '未设置项目 Key' };
89
+ try {
90
+ const baseUrl = await resolveFirst(['JIRA_BASE_URL', 'JIRA_URL']);
91
+ if (!baseUrl) return { ok: false, error: '未配置环境变量 JIRA_BASE_URL(或 JIRA_URL)' };
92
+ const token = await resolveFirst(['JIRA_API_TOKEN', 'JIRA_TOKEN']);
93
+ if (!token) return { ok: false, error: '未配置环境变量 JIRA_API_TOKEN(或 JIRA_TOKEN)' };
94
+
95
+ const jql = buildJql(args && args.jql ? String(args.jql) : '', projectKey);
96
+ const raw = await queryJira(baseUrl, token, jql);
97
+ const stdout = raw.stdout;
98
+ const stderr = raw.stderr;
99
+
100
+ if (raw.exitCode !== 0) {
101
+ let message = stderr.trim() || ('curl 退出码 ' + String(raw.exitCode));
102
+ try {
103
+ const parsed = JSON.parse(stdout);
104
+ if (parsed.errorMessages && parsed.errorMessages.length) message = parsed.errorMessages.join(';');
105
+ else if (parsed.message) message = parsed.message;
106
+ } catch (e) { /* stdout 非 JSON */ }
107
+ return { ok: false, error: message };
108
+ }
109
+
110
+ let data;
111
+ try {
112
+ data = JSON.parse(stdout);
113
+ } catch (e) {
114
+ return { ok: false, error: '无法解析 JIRA 响应:' + (stderr.trim() || '空响应') };
115
+ }
116
+
117
+ const issues = (data.issues || []).map((issue) => ({
118
+ key: issue.key || '',
119
+ summary: (issue.fields && issue.fields.summary) || '',
120
+ status: (issue.fields && issue.fields.status && issue.fields.status.name) || '',
121
+ statusCategory: (issue.fields && issue.fields.statuscategorykey) || '',
122
+ priority: (issue.fields && issue.fields.priority && issue.fields.priority.name) || '',
123
+ issueType: (issue.fields && issue.fields.issuetype && issue.fields.issuetype.name) || ''
124
+ }));
125
+
126
+ return {
127
+ ok: true,
128
+ baseUrl: baseUrl.replace(/\/+$/, ''),
129
+ projectKey,
130
+ total: typeof data.total === 'number' ? data.total : issues.length,
131
+ issues
132
+ };
133
+ } catch (err) {
134
+ return { ok: false, error: String((err && err.message) || err) };
135
+ }
136
+ }
137
+
138
+ if (webServer && typeof webServer.register === 'function') {
139
+ webServer.register({
140
+ kind: 'exact',
141
+ path: '/jira/api/search',
142
+ handler: async (req, res) => {
143
+ try {
144
+ sendJson(res, await handleSearch(await readBody(req)));
145
+ } catch (e) {
146
+ sendJson(res, { ok: false, error: String((e && e.message) || e) });
147
+ }
148
+ }
149
+ });
150
+ }
151
+ }
152
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "dsh-jira-tasks",
3
+ "version": "1.0.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.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "cordis.patch.yml",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "keywords": [
19
+ "dsh",
20
+ "deepseek-harness",
21
+ "jira",
22
+ "plugin",
23
+ "cordis",
24
+ "tasks"
25
+ ],
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/liu3734/jira-tasks-dsh-plugin.git"
30
+ },
31
+ "dsh": {
32
+ "bundle": {
33
+ "patch": "./cordis.patch.yml"
34
+ },
35
+ "client": {
36
+ "platform": "web",
37
+ "inject": [
38
+ "@deepseek-ai/dsh-client-ui-conversation"
39
+ ]
40
+ }
41
+ }
42
+ }