@co0ontty/wand 4.57.0 → 4.58.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/dist/build-info.json +3 -3
- package/dist/ensure-node-pty-helper.d.ts +0 -1
- package/dist/ensure-node-pty-helper.js +0 -5
- package/dist/git-quick-commit.js +18 -19
- package/dist/github-connector.d.ts +11 -5
- package/dist/github-connector.js +105 -33
- package/dist/missions.d.ts +1 -1
- package/dist/missions.js +17 -4
- package/dist/server-file-routes.js +43 -2
- package/dist/server-github-routes.js +50 -53
- package/dist/server-mission-routes.js +11 -10
- package/dist/server-task-routes.d.ts +14 -2
- package/dist/server-task-routes.js +248 -84
- package/dist/server-workspace-routes.js +90 -16
- package/dist/server.js +2 -20
- package/dist/storage.d.ts +23 -6
- package/dist/storage.js +130 -53
- package/dist/structured-exec-host.d.ts +13 -2
- package/dist/structured-exec-host.js +71 -36
- package/dist/structured-exec-pump.d.ts +1 -1
- package/dist/structured-exec-pump.js +14 -8
- package/dist/task-types.d.ts +15 -0
- package/dist/terminal-daemon-client.d.ts +2 -0
- package/dist/terminal-daemon-client.js +7 -1
- package/dist/terminal-daemon-server.js +17 -18
- package/dist/tui/ipc-server.js +3 -1
- package/dist/types.d.ts +1 -0
- package/dist/wand-task-sync.d.ts +9 -0
- package/dist/wand-task-sync.js +50 -0
- package/dist/web-ui/content/scripts.js +85 -85
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.js +5 -1
- package/package.json +2 -3
- package/dist/taskboard-bridge.d.ts +0 -11
- package/dist/taskboard-bridge.js +0 -371
- package/vendor/codex-taskboard/LICENSE +0 -201
- package/vendor/codex-taskboard/dist/web/assets/AiChat-BNn4VJ5a.js +0 -12
- package/vendor/codex-taskboard/dist/web/assets/GanttView-D8wnfGrG.css +0 -1
- package/vendor/codex-taskboard/dist/web/assets/GanttView-rGWhuEX9.js +0 -39
- package/vendor/codex-taskboard/dist/web/assets/abnfDiagram-N423BO3Z-DbFG6lbA.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/arc-BjSQqbzd.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/architecture-TIHT7OUA-C9-t8p4o.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/architectureDiagram-T3A2C74G-Db4kJZwO.js +0 -36
- package/vendor/codex-taskboard/dist/web/assets/array-BifhSqXX.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/blockDiagram-VBNYF7ZC-C-BywuWV.js +0 -132
- package/vendor/codex-taskboard/dist/web/assets/c4Diagram-5PPSVZJV-Dq-80rjP.js +0 -10
- package/vendor/codex-taskboard/dist/web/assets/channel-C7mnz8Xc.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-2GRJ4B5K-B2G57us9.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-3NCLNEKW-DVGIRib_.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-4I5QYGJK-kNoGFqr0.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-5RXB4S5H-4iv6-xM6.js +0 -231
- package/vendor/codex-taskboard/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +0 -15
- package/vendor/codex-taskboard/dist/web/assets/chunk-6Q2QTUOP-1J34vxHr.js +0 -88
- package/vendor/codex-taskboard/dist/web/assets/chunk-7BUUIJ7U-Bb538aSH.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-7Z6QIM7H-CDQpQlxX.js +0 -10
- package/vendor/codex-taskboard/dist/web/assets/chunk-GF5L2VYU-Dg92oHHl.js +0 -206
- package/vendor/codex-taskboard/dist/web/assets/chunk-I66GZJ75-Bk8_gFs2.js +0 -125
- package/vendor/codex-taskboard/dist/web/assets/chunk-J7OUQ5F2-CJRcenlr.js +0 -2
- package/vendor/codex-taskboard/dist/web/assets/chunk-JQJVKLGR-BLBV_Sus.js +0 -156
- package/vendor/codex-taskboard/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-KBJHAD2P-Da9Yjz0p.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-KEIR6QF5-Dj-OpFgW.js +0 -161
- package/vendor/codex-taskboard/dist/web/assets/chunk-NSK5VX7P-Cb56tnGK.js +0 -2
- package/vendor/codex-taskboard/dist/web/assets/chunk-QR6OTTB3-BwoszTxn.js +0 -62
- package/vendor/codex-taskboard/dist/web/assets/chunk-RYQCIY6F-D_L2RdcQ.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-UBXNYLIW-YX5mN8cl.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-W5SLKNZC-DlOLhhDO.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-WRU74C26-B7SBmP0K.js +0 -70
- package/vendor/codex-taskboard/dist/web/assets/chunk-XXDRQBXY-BuE3VzE_.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/chunk-ZIRB5QZD-C6fEPe3t.js +0 -32
- package/vendor/codex-taskboard/dist/web/assets/classDiagram-JCYQIIEL-KIdCVvbA.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/classDiagram-v2-OCEON4UE-KIdCVvbA.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/cose-bilkent-JH36ORCC-BzQvy5TC.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/cynefin-VYW2F7L2-D1_5O1XR.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/cynefinDiagram-MW4NZA55-DOW_U5QN.js +0 -62
- package/vendor/codex-taskboard/dist/web/assets/cytoscape.esm-DnGd5Fuo.js +0 -321
- package/vendor/codex-taskboard/dist/web/assets/dagre-VZM6K2ZE-0Dntc3NT.js +0 -4
- package/vendor/codex-taskboard/dist/web/assets/dagre-dpRSp0QF.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/defaultLocale-C8Fc0cco.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/diagram-7IWD3JNH-D7GleYAM.js +0 -30
- package/vendor/codex-taskboard/dist/web/assets/diagram-B4RE2ZJO-DCXTe6q5.js +0 -3
- package/vendor/codex-taskboard/dist/web/assets/diagram-LBJQPF4R-ZFJyXEXl.js +0 -24
- package/vendor/codex-taskboard/dist/web/assets/diagram-Q27KOJAE-DUGCuGd9.js +0 -24
- package/vendor/codex-taskboard/dist/web/assets/diagram-UB23O5K3-DRmp4Nng.js +0 -41
- package/vendor/codex-taskboard/dist/web/assets/dist-D9sYb5Oa.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/ebnfDiagram-BXEA7PRR-CkKoaf7T.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/erDiagram-JOGREHBK-nBX5z4R4.js +0 -85
- package/vendor/codex-taskboard/dist/web/assets/eventmodeling-45OFAUF4-pexmK65N.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/flowDiagram-UKHOOZJN-aroohRbL.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/ganttDiagram-PKOTCBZU-BOJWUTXa.js +0 -292
- package/vendor/codex-taskboard/dist/web/assets/gitGraph-TEB2WS4Q-DxnuhSZU.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/gitGraphDiagram-DS77QQ5N-Cte975TC.js +0 -106
- package/vendor/codex-taskboard/dist/web/assets/graphlib-DS17s2tU.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/index-Docmx4uQ.css +0 -1
- package/vendor/codex-taskboard/dist/web/assets/index-Du07lEGz.js +0 -121
- package/vendor/codex-taskboard/dist/web/assets/info-DKCQHKI2-DOTjgYxk.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/infoDiagram-6WML65LV-SpyV9UJd.js +0 -2
- package/vendor/codex-taskboard/dist/web/assets/init-D6jRqBbL.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/ishikawaDiagram-WSZJBQD7-T0PZwoUZ.js +0 -70
- package/vendor/codex-taskboard/dist/web/assets/journeyDiagram-NVQOT4AX-B88b8UgX.js +0 -139
- package/vendor/codex-taskboard/dist/web/assets/kanban-definition-27J2QSJJ-CIlfNnl_.js +0 -89
- package/vendor/codex-taskboard/dist/web/assets/katex-B7rAX3Vi.js +0 -257
- package/vendor/codex-taskboard/dist/web/assets/line-fWrwPZhB.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/linear-B7l8qgEw.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/map-BaFkSB1l.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/mermaid-parser.core-C5gls2nC.js +0 -7
- package/vendor/codex-taskboard/dist/web/assets/mermaid.core-POz2Xcvr.js +0 -11
- package/vendor/codex-taskboard/dist/web/assets/mindmap-definition-FAOFIHXS-BM0-egZ5.js +0 -96
- package/vendor/codex-taskboard/dist/web/assets/ordinal-hYBb2elL.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/packet-7NZHBO7P-DQZzyVos.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/path-BWPyau1x.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/pegDiagram-VL7TDLO6-B8aEwl-7.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/pie-RZYD4A2V-DRUflilR.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/pieDiagram-7S7Q4E2Y-0cDDxlua.js +0 -39
- package/vendor/codex-taskboard/dist/web/assets/purify.es-Bwt4ulIK.js +0 -3
- package/vendor/codex-taskboard/dist/web/assets/quadrantDiagram-CIZ2JOQS-CBgb0YZ0.js +0 -7
- package/vendor/codex-taskboard/dist/web/assets/radar-I7S5WNFK-tRocoKxq.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroad-3IZDKUUU-DaS4ZlX_.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroad-abnf-AHOZXSZD-BN1T5dBT.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroad-ebnf-EBAXGLYW-Cuf8qyKs.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroad-peg-LSFZ7HO6-B511_EYq.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/railroadDiagram-AXF67PYL-QtPo-JZb.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/requirementDiagram-LRYGKXZP-CuBIF85r.js +0 -84
- package/vendor/codex-taskboard/dist/web/assets/rolldown-runtime-QTnfLwEv.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/rough.esm-CSKSodPl.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/sankeyDiagram-W5VNT64P-CRA-b_za.js +0 -40
- package/vendor/codex-taskboard/dist/web/assets/sequenceDiagram-SI44F4Z6-CoddVWwY.js +0 -162
- package/vendor/codex-taskboard/dist/web/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/src-_wZywoZs.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/stateDiagram-OKZ733FA-D8DMM2ln.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/stateDiagram-v2-UEYNNEHI-CVp0eYAS.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/swimlanes-SLNWSIFB-C1fC7EFu.js +0 -2
- package/vendor/codex-taskboard/dist/web/assets/swimlanesDiagram-ULZ7WXOC-CPd58Xso.js +0 -8
- package/vendor/codex-taskboard/dist/web/assets/timeline-definition-Z64GVDOM-BUBv8aKP.js +0 -120
- package/vendor/codex-taskboard/dist/web/assets/treeView-QDETBFTQ-C-Bve2kY.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/treemap-6X3UGDF4-BcphHDdV.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/vennDiagram-T6HMQDX7-0wQnwAyA.js +0 -34
- package/vendor/codex-taskboard/dist/web/assets/wardley-OPB4EBWU-CJZ4BevB.js +0 -1
- package/vendor/codex-taskboard/dist/web/assets/wardleyDiagram-T6FBY63Y-U9nwPrkN.js +0 -78
- package/vendor/codex-taskboard/dist/web/assets/xychartDiagram-ELKLHX3M-CyCqnzgE.js +0 -7
- package/vendor/codex-taskboard/dist/web/codex-agent-logo.png +0 -0
- package/vendor/codex-taskboard/dist/web/codex-app-icon.png +0 -0
- package/vendor/codex-taskboard/dist/web/favicon.svg +0 -7
- package/vendor/codex-taskboard/dist/web/index.html +0 -20
- package/vendor/codex-taskboard/server/ai-chat-catalog.mjs +0 -1031
- package/vendor/codex-taskboard/server/ai-chat-process.mjs +0 -462
- package/vendor/codex-taskboard/server/ai-chat.mjs +0 -1237
- package/vendor/codex-taskboard/server/ai-turn-owner.mjs +0 -39
- package/vendor/codex-taskboard/server/app.mjs +0 -3211
- package/vendor/codex-taskboard/server/cloud-config.mjs +0 -188
- package/vendor/codex-taskboard/server/cloud-proxy.mjs +0 -310
- package/vendor/codex-taskboard/server/codex-app-server.mjs +0 -376
- package/vendor/codex-taskboard/server/codex-slash-commands-0.139.0.json +0 -62
- package/vendor/codex-taskboard/server/database.mjs +0 -2845
- package/vendor/codex-taskboard/server/index.mjs +0 -41
- package/vendor/codex-taskboard/server/jira-config.mjs +0 -181
- package/vendor/codex-taskboard/server/jira-integration.mjs +0 -487
- package/vendor/codex-taskboard/server/project-summary.mjs +0 -159
- package/vendor/codex-taskboard/shared/api-fields.mjs +0 -163
- package/vendor/codex-taskboard/shared/codex-environment.mjs +0 -5
- package/vendor/codex-taskboard/shared/codex-executable.mjs +0 -77
- package/vendor/codex-taskboard/shared/codex-permissions.mjs +0 -9
- package/vendor/codex-taskboard/shared/composer-reference.d.mts +0 -19
- package/vendor/codex-taskboard/shared/composer-reference.mjs +0 -90
- package/vendor/codex-taskboard/shared/domain.mjs +0 -34
- package/vendor/codex-taskboard/shared/executable-command.mjs +0 -13
- package/vendor/codex-taskboard/shared/process-tree.mjs +0 -27
- package/vendor/codex-taskboard/shared/task-input.mjs +0 -308
- package/vendor/codex-taskboard/shared/task-records.mjs +0 -199
- package/vendor/codex-taskboard/shared/task-relations.mjs +0 -55
- package/vendor/codex-taskboard/shared/taskboard-automation.mjs +0 -277
- package/vendor/codex-taskboard/skills/manage-taskboard/SKILL.md +0 -45
- package/vendor/codex-taskboard/skills/manage-taskboard/agents/openai.yaml +0 -4
- package/vendor/codex-taskboard/skills/manage-taskboard/references/cli.md +0 -211
|
@@ -1,487 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
import { JIRA_PROJECT_ID } from "../shared/domain.mjs";
|
|
4
|
-
import { ApiError } from "../shared/api-fields.mjs";
|
|
5
|
-
|
|
6
|
-
const JIRA_FIELDS = [
|
|
7
|
-
"summary",
|
|
8
|
-
"description",
|
|
9
|
-
"status",
|
|
10
|
-
"priority",
|
|
11
|
-
"labels",
|
|
12
|
-
"duedate",
|
|
13
|
-
"assignee",
|
|
14
|
-
"reporter",
|
|
15
|
-
"created",
|
|
16
|
-
"updated",
|
|
17
|
-
];
|
|
18
|
-
const SYNC_INTERVAL_MS = 60_000;
|
|
19
|
-
const REQUEST_TIMEOUT_MS = 20_000;
|
|
20
|
-
|
|
21
|
-
function quoteJqlString(value) {
|
|
22
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function buildJiraJql(projects = []) {
|
|
26
|
-
const projectFilter = projects.length > 0
|
|
27
|
-
? ` AND project in (${projects.map(quoteJqlString).join(", ")})`
|
|
28
|
-
: "";
|
|
29
|
-
return `assignee = currentUser()${projectFilter} AND (statusCategory != Done OR updated >= -30d) ORDER BY updated DESC`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function includesAny(value, terms) {
|
|
33
|
-
const normalized = String(value ?? "").toLowerCase();
|
|
34
|
-
return terms.some((term) => normalized.includes(term));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function limitedString(value, fallback, maxLength) {
|
|
38
|
-
const result = String(value ?? fallback).trim();
|
|
39
|
-
return (result || fallback).slice(0, maxLength);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function taskStatusFromJira(status) {
|
|
43
|
-
const name = status?.name ?? "";
|
|
44
|
-
const category = status?.statusCategory?.key;
|
|
45
|
-
if (category === "done") {
|
|
46
|
-
return includesAny(name, ["cancel", "reject", "取消", "拒绝"]) ? "canceled" : "done";
|
|
47
|
-
}
|
|
48
|
-
if (category === "new") {
|
|
49
|
-
return includesAny(name, ["backlog", "待立项", "需求池"]) ? "backlog" : "todo";
|
|
50
|
-
}
|
|
51
|
-
if (includesAny(name, ["review", "verify", "test", "验收", "评审", "测试"])) {
|
|
52
|
-
return "in_review";
|
|
53
|
-
}
|
|
54
|
-
if (includesAny(name, ["block", "hold", "阻塞", "挂起"])) return "blocked";
|
|
55
|
-
return "in_progress";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function taskPriorityFromJira(priority) {
|
|
59
|
-
const name = priority?.name ?? "";
|
|
60
|
-
if (includesAny(name, ["highest", "critical", "blocker", "urgent", "紧急", "最高"])) {
|
|
61
|
-
return "urgent";
|
|
62
|
-
}
|
|
63
|
-
if (includesAny(name, ["high", "major", "高"])) return "high";
|
|
64
|
-
if (includesAny(name, ["medium", "normal", "中"])) return "medium";
|
|
65
|
-
if (includesAny(name, ["low", "minor", "trivial", "低"])) return "low";
|
|
66
|
-
return "none";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function actorFromJira(user, fallback) {
|
|
70
|
-
const id = limitedString(user?.key ?? user?.name ?? user?.accountId, fallback, 240);
|
|
71
|
-
return {
|
|
72
|
-
type: "user",
|
|
73
|
-
id: `jira:${id}`,
|
|
74
|
-
name: limitedString(user?.displayName ?? user?.name, fallback, 120),
|
|
75
|
-
avatarUrl: user?.avatarUrls?.["48x48"] ?? user?.avatarUrls?.["32x32"] ?? null,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function jiraOriginId(manifest) {
|
|
80
|
-
const applicationId = typeof manifest?.id === "string" ? manifest.id.trim() : "";
|
|
81
|
-
if (!applicationId) {
|
|
82
|
-
throw new ApiError(502, "INVALID_JIRA_ORIGIN", "Jira 未返回稳定的实例身份");
|
|
83
|
-
}
|
|
84
|
-
return createHash("sha256").update(applicationId).digest("hex");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function legacyJiraOriginId(baseUrl) {
|
|
88
|
-
return createHash("sha256").update(baseUrl).digest("hex").slice(0, 16);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function normalizeIssue(issue, config, index = 0) {
|
|
92
|
-
const fields = issue?.fields ?? {};
|
|
93
|
-
const externalId = String(issue.id);
|
|
94
|
-
const externalKey = limitedString(issue.key, "JIRA", 128);
|
|
95
|
-
const internalId = `JIRA:${config.originId.toUpperCase()}:${externalId}`;
|
|
96
|
-
const assignee = actorFromJira(fields.assignee, config.displayName);
|
|
97
|
-
const reporter = actorFromJira(fields.reporter, config.displayName);
|
|
98
|
-
const labels = Array.isArray(fields.labels)
|
|
99
|
-
? [...new Set(fields.labels.flatMap((label) => {
|
|
100
|
-
if (typeof label !== "string") return [];
|
|
101
|
-
const normalized = label.trim().slice(0, 64);
|
|
102
|
-
return normalized ? [normalized] : [];
|
|
103
|
-
}))].slice(0, 20)
|
|
104
|
-
: [];
|
|
105
|
-
return {
|
|
106
|
-
id: internalId,
|
|
107
|
-
identifier: internalId,
|
|
108
|
-
title: limitedString(fields.summary, externalKey, 240),
|
|
109
|
-
description: typeof fields.description === "string" ? fields.description.slice(0, 100_000) : "",
|
|
110
|
-
status: taskStatusFromJira(fields.status),
|
|
111
|
-
priority: taskPriorityFromJira(fields.priority),
|
|
112
|
-
labels,
|
|
113
|
-
sortOrder: (index + 1) * 1024,
|
|
114
|
-
creator: reporter,
|
|
115
|
-
assignee,
|
|
116
|
-
dueDate: typeof fields.duedate === "string" ? fields.duedate : null,
|
|
117
|
-
externalOrigin: config.originId,
|
|
118
|
-
externalId,
|
|
119
|
-
externalKey,
|
|
120
|
-
externalUrl: `${config.baseUrl}/browse/${encodeURIComponent(externalKey)}`,
|
|
121
|
-
createdAt: typeof fields.created === "string" ? fields.created : new Date().toISOString(),
|
|
122
|
-
updatedAt: typeof fields.updated === "string" ? fields.updated : new Date().toISOString(),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function safeConfig(config, lastSyncedAt = null) {
|
|
127
|
-
return config
|
|
128
|
-
? {
|
|
129
|
-
configured: true,
|
|
130
|
-
baseUrl: config.baseUrl,
|
|
131
|
-
username: null,
|
|
132
|
-
displayName: config.displayName,
|
|
133
|
-
projects: config.projects,
|
|
134
|
-
projectId: JIRA_PROJECT_ID,
|
|
135
|
-
lastSyncedAt,
|
|
136
|
-
insecureHttp: config.baseUrl.startsWith("http:"),
|
|
137
|
-
}
|
|
138
|
-
: {
|
|
139
|
-
configured: false,
|
|
140
|
-
baseUrl: null,
|
|
141
|
-
username: null,
|
|
142
|
-
displayName: null,
|
|
143
|
-
projects: [],
|
|
144
|
-
projectId: JIRA_PROJECT_ID,
|
|
145
|
-
lastSyncedAt: null,
|
|
146
|
-
insecureHttp: false,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function createJiraIntegration({ configStore, database, fetch: fetchImplementation = globalThis.fetch }) {
|
|
151
|
-
let lastSyncedAt = null;
|
|
152
|
-
let pendingSync = null;
|
|
153
|
-
let pendingOperation = Promise.resolve();
|
|
154
|
-
|
|
155
|
-
function runInOrder(operation) {
|
|
156
|
-
const result = pendingOperation.then(operation);
|
|
157
|
-
// Recover only the scheduling tail; callers still receive the rejection.
|
|
158
|
-
pendingOperation = result.catch(() => {});
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function request(config, pathname, init = {}) {
|
|
163
|
-
const controller = new AbortController();
|
|
164
|
-
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
165
|
-
timeout.unref?.();
|
|
166
|
-
let response;
|
|
167
|
-
try {
|
|
168
|
-
response = await fetchImplementation(`${config.baseUrl}${pathname}`, {
|
|
169
|
-
...init,
|
|
170
|
-
redirect: "manual",
|
|
171
|
-
signal: controller.signal,
|
|
172
|
-
headers: {
|
|
173
|
-
accept: "application/json",
|
|
174
|
-
authorization: `Basic ${Buffer.from(`${config.username}:${config.password}`, "utf8").toString("base64")}`,
|
|
175
|
-
...(init.body ? { "content-type": "application/json" } : {}),
|
|
176
|
-
...init.headers,
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
} catch (error) {
|
|
180
|
-
const timedOut = error instanceof Error && error.name === "AbortError";
|
|
181
|
-
throw new ApiError(
|
|
182
|
-
502,
|
|
183
|
-
timedOut ? "JIRA_TIMEOUT" : "JIRA_UNAVAILABLE",
|
|
184
|
-
timedOut ? "连接 Jira 超时" : "无法连接 Jira,请检查地址和内网连接",
|
|
185
|
-
);
|
|
186
|
-
} finally {
|
|
187
|
-
clearTimeout(timeout);
|
|
188
|
-
}
|
|
189
|
-
if (response.status === 401 || response.status === 403) {
|
|
190
|
-
throw new ApiError(
|
|
191
|
-
401,
|
|
192
|
-
"JIRA_AUTH_FAILED",
|
|
193
|
-
"Jira 登录失败,请检查用户名、密码、Basic Auth 或 CAPTCHA 状态",
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
if (response.status >= 300 && response.status < 400) {
|
|
197
|
-
throw new ApiError(400, "JIRA_REDIRECT", "Jira 地址发生重定向,请填写最终访问地址");
|
|
198
|
-
}
|
|
199
|
-
if (!response.ok) {
|
|
200
|
-
throw new ApiError(
|
|
201
|
-
response.status >= 500 ? 502 : 409,
|
|
202
|
-
"JIRA_REQUEST_FAILED",
|
|
203
|
-
`Jira 请求失败(HTTP ${response.status})`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
if (response.status === 204) return null;
|
|
207
|
-
try {
|
|
208
|
-
return await response.json();
|
|
209
|
-
} catch {
|
|
210
|
-
throw new ApiError(502, "INVALID_JIRA_RESPONSE", "Jira 返回了无效的 JSON 数据");
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function fetchAssignedIssues(config) {
|
|
215
|
-
const issues = [];
|
|
216
|
-
let startAt = 0;
|
|
217
|
-
while (true) {
|
|
218
|
-
const page = await request(config, "/rest/api/2/search", {
|
|
219
|
-
method: "POST",
|
|
220
|
-
body: JSON.stringify({
|
|
221
|
-
jql: buildJiraJql(config.projects),
|
|
222
|
-
startAt,
|
|
223
|
-
maxResults: 100,
|
|
224
|
-
fields: JIRA_FIELDS,
|
|
225
|
-
}),
|
|
226
|
-
});
|
|
227
|
-
const pageIssues = Array.isArray(page?.issues) ? page.issues : [];
|
|
228
|
-
issues.push(...pageIssues);
|
|
229
|
-
startAt += pageIssues.length;
|
|
230
|
-
if (pageIssues.length === 0 || startAt >= Number(page?.total ?? 0)) break;
|
|
231
|
-
}
|
|
232
|
-
return issues;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function fetchOriginId(config) {
|
|
236
|
-
return jiraOriginId(await request(config, "/rest/applinks/1.0/manifest"));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async function assertLiveOrigin(config) {
|
|
240
|
-
if (await fetchOriginId(config) !== config.originId) {
|
|
241
|
-
throw new ApiError(
|
|
242
|
-
409,
|
|
243
|
-
"JIRA_ORIGIN_MISMATCH",
|
|
244
|
-
"当前 Jira 地址指向了另一个实例,请重新连接后再操作",
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function validateConnection(candidate) {
|
|
250
|
-
const originId = await fetchOriginId(candidate);
|
|
251
|
-
const myself = await request(candidate, "/rest/api/2/myself");
|
|
252
|
-
const displayName = String(myself?.displayName ?? myself?.name ?? candidate.username).trim();
|
|
253
|
-
const config = { ...candidate, originId, displayName };
|
|
254
|
-
const issues = await fetchAssignedIssues(config);
|
|
255
|
-
return { config, issues };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function syncWithConfig(storedConfig, { archiveMissing = true } = {}) {
|
|
259
|
-
let config = storedConfig;
|
|
260
|
-
let issues;
|
|
261
|
-
let legacyIdentity = null;
|
|
262
|
-
if (storedConfig.version === 1) {
|
|
263
|
-
({ config, issues } = await validateConnection(storedConfig));
|
|
264
|
-
legacyIdentity = {
|
|
265
|
-
urlHash: legacyJiraOriginId(storedConfig.baseUrl),
|
|
266
|
-
originId: config.originId,
|
|
267
|
-
};
|
|
268
|
-
} else {
|
|
269
|
-
await assertLiveOrigin(config);
|
|
270
|
-
issues = await fetchAssignedIssues(config);
|
|
271
|
-
}
|
|
272
|
-
database.syncJiraTasks(
|
|
273
|
-
issues.map((issue, index) => normalizeIssue(issue, config, index)),
|
|
274
|
-
{ archiveMissing, projectName: `Jira · ${config.displayName}`, legacyIdentity },
|
|
275
|
-
);
|
|
276
|
-
if (storedConfig.version === 1) config = await configStore.save(config);
|
|
277
|
-
lastSyncedAt = new Date().toISOString();
|
|
278
|
-
return safeConfig(config, lastSyncedAt);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function sync({ force = false } = {}) {
|
|
282
|
-
if (pendingSync) {
|
|
283
|
-
// A forced caller must not inherit a queued, throttled no-op.
|
|
284
|
-
pendingSync.force ||= force;
|
|
285
|
-
return pendingSync.promise;
|
|
286
|
-
}
|
|
287
|
-
const syncRequest = { force, promise: null };
|
|
288
|
-
syncRequest.promise = runInOrder(async () => {
|
|
289
|
-
try {
|
|
290
|
-
const config = await configStore.read();
|
|
291
|
-
if (!config) return safeConfig(null);
|
|
292
|
-
if (!syncRequest.force && lastSyncedAt && Date.now() - new Date(lastSyncedAt).getTime() < SYNC_INTERVAL_MS) {
|
|
293
|
-
return safeConfig(config, lastSyncedAt);
|
|
294
|
-
}
|
|
295
|
-
return await syncWithConfig(config);
|
|
296
|
-
} finally {
|
|
297
|
-
pendingSync = null;
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
pendingSync = syncRequest;
|
|
301
|
-
return syncRequest.promise;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async function resolveTransition(config, issueKey, targetStatus) {
|
|
305
|
-
const payload = await request(
|
|
306
|
-
config,
|
|
307
|
-
`/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions?expand=transitions.fields`,
|
|
308
|
-
);
|
|
309
|
-
const transitions = Array.isArray(payload?.transitions) ? payload.transitions : [];
|
|
310
|
-
const matches = transitions.filter((candidate) => taskStatusFromJira(candidate.to) === targetStatus);
|
|
311
|
-
const availableStatuses = transitions.map((candidate) => ({
|
|
312
|
-
id: String(candidate.id),
|
|
313
|
-
name: String(candidate.name ?? candidate.to?.name ?? ""),
|
|
314
|
-
taskboardStatus: taskStatusFromJira(candidate.to),
|
|
315
|
-
}));
|
|
316
|
-
if (matches.length === 0) {
|
|
317
|
-
throw new ApiError(
|
|
318
|
-
409,
|
|
319
|
-
"JIRA_TRANSITION_UNAVAILABLE",
|
|
320
|
-
`Jira 当前工作流不能将 ${issueKey} 移到目标状态`,
|
|
321
|
-
{ availableStatuses },
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
if (matches.length > 1) {
|
|
325
|
-
throw new ApiError(
|
|
326
|
-
409,
|
|
327
|
-
"JIRA_TRANSITION_AMBIGUOUS",
|
|
328
|
-
`Jira 有多个工作流操作可将 ${issueKey} 移到目标状态,请在 Jira 中选择`,
|
|
329
|
-
{
|
|
330
|
-
availableStatuses: availableStatuses.filter(
|
|
331
|
-
(candidate) => candidate.taskboardStatus === targetStatus,
|
|
332
|
-
),
|
|
333
|
-
},
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
return matches[0];
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
async function applyTransition(config, issueKey, transition) {
|
|
340
|
-
await request(config, `/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions`, {
|
|
341
|
-
method: "POST",
|
|
342
|
-
body: JSON.stringify({ transition: { id: String(transition.id) } }),
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async function resolveJiraPriority(config, targetPriority) {
|
|
347
|
-
if (targetPriority === "none") return null;
|
|
348
|
-
const priorities = await request(config, "/rest/api/2/priority");
|
|
349
|
-
const match = Array.isArray(priorities)
|
|
350
|
-
? priorities.find((priority) => taskPriorityFromJira(priority) === targetPriority)
|
|
351
|
-
: null;
|
|
352
|
-
if (!match) {
|
|
353
|
-
throw new ApiError(
|
|
354
|
-
409,
|
|
355
|
-
"JIRA_PRIORITY_UNAVAILABLE",
|
|
356
|
-
"Jira 中没有可映射到该优先级的选项",
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
return { id: String(match.id) };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return {
|
|
363
|
-
async status() {
|
|
364
|
-
return safeConfig(await configStore.read(), lastSyncedAt);
|
|
365
|
-
},
|
|
366
|
-
async configure(input) {
|
|
367
|
-
return runInOrder(async () => {
|
|
368
|
-
const current = await configStore.read();
|
|
369
|
-
const username = input.username || current?.username;
|
|
370
|
-
const password = input.password || current?.password;
|
|
371
|
-
const candidate = configStore.validate({ ...input, username, password });
|
|
372
|
-
if (current?.version === 1 && candidate.baseUrl !== current.baseUrl) {
|
|
373
|
-
throw new ApiError(
|
|
374
|
-
409,
|
|
375
|
-
"JIRA_LEGACY_URL_CHANGE_UNAVAILABLE",
|
|
376
|
-
"请先使用原 Jira 地址完成配置升级,再修改地址",
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
if (
|
|
380
|
-
!input.password
|
|
381
|
-
&& (
|
|
382
|
-
!current
|
|
383
|
-
|| candidate.baseUrl !== current.baseUrl
|
|
384
|
-
|| candidate.username !== current.username
|
|
385
|
-
)
|
|
386
|
-
) {
|
|
387
|
-
throw new ApiError(
|
|
388
|
-
400,
|
|
389
|
-
"JIRA_PASSWORD_REQUIRED",
|
|
390
|
-
"修改 Jira 地址或用户名时必须重新输入密码",
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
const { config, issues } = await validateConnection(candidate);
|
|
394
|
-
const legacyIdentity = current?.version === 1
|
|
395
|
-
? { urlHash: legacyJiraOriginId(current.baseUrl), originId: config.originId }
|
|
396
|
-
: null;
|
|
397
|
-
database.syncJiraTasks(
|
|
398
|
-
issues.map((issue, index) => normalizeIssue(issue, config, index)),
|
|
399
|
-
{
|
|
400
|
-
archiveMissing: true,
|
|
401
|
-
projectName: `Jira · ${config.displayName}`,
|
|
402
|
-
legacyIdentity,
|
|
403
|
-
},
|
|
404
|
-
);
|
|
405
|
-
const savedConfig = await configStore.save(config);
|
|
406
|
-
lastSyncedAt = new Date().toISOString();
|
|
407
|
-
return safeConfig(savedConfig, lastSyncedAt);
|
|
408
|
-
});
|
|
409
|
-
},
|
|
410
|
-
sync,
|
|
411
|
-
async reconcile() {
|
|
412
|
-
return runInOrder(async () => {
|
|
413
|
-
const config = await configStore.read();
|
|
414
|
-
if (!config || config.version !== 2) {
|
|
415
|
-
throw new ApiError(409, "JIRA_NOT_CONFIGURED", "Jira 尚未完成稳定身份配置");
|
|
416
|
-
}
|
|
417
|
-
return syncWithConfig(config, { archiveMissing: false });
|
|
418
|
-
});
|
|
419
|
-
},
|
|
420
|
-
async updateTask(task, changes) {
|
|
421
|
-
const config = await configStore.read();
|
|
422
|
-
if (!config) throw new ApiError(409, "JIRA_NOT_CONFIGURED", "Jira 尚未配置");
|
|
423
|
-
if (task.externalOrigin !== config.originId || !task.externalKey) {
|
|
424
|
-
throw new ApiError(
|
|
425
|
-
409,
|
|
426
|
-
"JIRA_ORIGIN_MISMATCH",
|
|
427
|
-
"此任务不属于当前 Jira 连接,请重新同步后再操作",
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
await assertLiveOrigin(config);
|
|
431
|
-
const statusChanged = Object.hasOwn(changes, "status") && changes.status !== task.status;
|
|
432
|
-
const priorityChanged = Object.hasOwn(changes, "priority") && changes.priority !== task.priority;
|
|
433
|
-
const fields = {};
|
|
434
|
-
if (Object.hasOwn(changes, "title") && changes.title !== task.title) fields.summary = changes.title;
|
|
435
|
-
if (Object.hasOwn(changes, "description") && changes.description !== task.description) {
|
|
436
|
-
fields.description = changes.description;
|
|
437
|
-
}
|
|
438
|
-
if (Object.hasOwn(changes, "labels") && JSON.stringify(changes.labels) !== JSON.stringify(task.labels)) {
|
|
439
|
-
fields.labels = changes.labels;
|
|
440
|
-
}
|
|
441
|
-
if (Object.hasOwn(changes, "dueDate") && changes.dueDate !== task.dueDate) {
|
|
442
|
-
fields.duedate = changes.dueDate;
|
|
443
|
-
}
|
|
444
|
-
const fieldsChanged = Object.keys(fields).length > 0 || priorityChanged;
|
|
445
|
-
if (statusChanged && fieldsChanged) {
|
|
446
|
-
throw new ApiError(
|
|
447
|
-
409,
|
|
448
|
-
"JIRA_MULTI_STEP_UPDATE_UNAVAILABLE",
|
|
449
|
-
"请分开修改 Jira 状态和其他字段",
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
if (priorityChanged) {
|
|
453
|
-
fields.priority = await resolveJiraPriority(config, changes.priority);
|
|
454
|
-
}
|
|
455
|
-
const transition = statusChanged
|
|
456
|
-
? await resolveTransition(config, task.externalKey, changes.status)
|
|
457
|
-
: null;
|
|
458
|
-
if (transition) {
|
|
459
|
-
await applyTransition(config, task.externalKey, transition);
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
if (fieldsChanged) {
|
|
463
|
-
await request(config, `/rest/api/2/issue/${encodeURIComponent(task.externalKey)}`, {
|
|
464
|
-
method: "PUT",
|
|
465
|
-
body: JSON.stringify({ fields }),
|
|
466
|
-
});
|
|
467
|
-
return true;
|
|
468
|
-
}
|
|
469
|
-
return false;
|
|
470
|
-
},
|
|
471
|
-
async moveTask(task, status) {
|
|
472
|
-
if (status === task.status) return;
|
|
473
|
-
const config = await configStore.read();
|
|
474
|
-
if (!config) throw new ApiError(409, "JIRA_NOT_CONFIGURED", "Jira 尚未配置");
|
|
475
|
-
if (task.externalOrigin !== config.originId || !task.externalKey) {
|
|
476
|
-
throw new ApiError(
|
|
477
|
-
409,
|
|
478
|
-
"JIRA_ORIGIN_MISMATCH",
|
|
479
|
-
"此任务不属于当前 Jira 连接,请重新同步后再操作",
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
await assertLiveOrigin(config);
|
|
483
|
-
const transition = await resolveTransition(config, task.externalKey, status);
|
|
484
|
-
await applyTransition(config, task.externalKey, transition);
|
|
485
|
-
},
|
|
486
|
-
};
|
|
487
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { signalProcessTree } from "../shared/process-tree.mjs";
|
|
2
|
-
import { spawnCodexTurn } from "./ai-chat-process.mjs";
|
|
3
|
-
import { ApiError } from "../shared/api-fields.mjs";
|
|
4
|
-
|
|
5
|
-
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
6
|
-
const CHECK_INTERVAL_MS = 60 * 60 * 1_000;
|
|
7
|
-
const STATUS_LABELS = {
|
|
8
|
-
backlog: "积压事项",
|
|
9
|
-
todo: "待办",
|
|
10
|
-
in_progress: "处理中",
|
|
11
|
-
in_review: "等你确认",
|
|
12
|
-
blocked: "遇到阻碍",
|
|
13
|
-
done: "完成",
|
|
14
|
-
canceled: "取消",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
function isDue(summary) {
|
|
18
|
-
if (!summary.attemptedAt) return true;
|
|
19
|
-
return Date.now() - new Date(summary.attemptedAt).getTime() >= DAY_MS;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function buildPrompt(project, tasks) {
|
|
23
|
-
const counts = Object.fromEntries(Object.keys(STATUS_LABELS).map((status) => [
|
|
24
|
-
STATUS_LABELS[status],
|
|
25
|
-
tasks.filter((task) => task.status === status).length,
|
|
26
|
-
]));
|
|
27
|
-
const recentCutoff = Date.now() - 7 * DAY_MS;
|
|
28
|
-
const issues = tasks.filter((task) => (
|
|
29
|
-
(task.status !== "done" && task.status !== "canceled")
|
|
30
|
-
|| new Date(task.activityUpdatedAt).getTime() >= recentCutoff
|
|
31
|
-
)).map((task) => ({
|
|
32
|
-
id: task.identifier,
|
|
33
|
-
title: task.title,
|
|
34
|
-
status: STATUS_LABELS[task.status],
|
|
35
|
-
priority: task.priority,
|
|
36
|
-
labels: task.labels,
|
|
37
|
-
dueDate: task.dueDate,
|
|
38
|
-
updatedAt: task.activityUpdatedAt,
|
|
39
|
-
}));
|
|
40
|
-
return [
|
|
41
|
-
"你是 Codex。请根据下面的任务面板快照,为项目负责人写一段项目总结。",
|
|
42
|
-
"要求:只输出一段 60 至 120 字的简体中文;直接说明当前进展、主要风险或阻碍、下一步重点;不要使用标题、列表或 Markdown;不要调用工具。",
|
|
43
|
-
JSON.stringify({
|
|
44
|
-
project: project.name,
|
|
45
|
-
generatedForDate: new Date().toISOString().slice(0, 10),
|
|
46
|
-
counts,
|
|
47
|
-
issues,
|
|
48
|
-
}),
|
|
49
|
-
].join("\n\n");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export class ProjectSummaryService {
|
|
53
|
-
constructor(options) {
|
|
54
|
-
this.database = options.database;
|
|
55
|
-
this.codexExecutable = options.codexExecutable;
|
|
56
|
-
this.workspacePath = options.workspacePath;
|
|
57
|
-
this.processEnv = options.processEnv ?? process.env;
|
|
58
|
-
this.active = new Map();
|
|
59
|
-
this.closed = false;
|
|
60
|
-
this.timer = setInterval(() => void this.refreshDueProjects(), CHECK_INTERVAL_MS);
|
|
61
|
-
this.timer.unref();
|
|
62
|
-
void this.refreshDueProjects();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
get(projectId) {
|
|
66
|
-
if (!this.database.getProject(projectId)) {
|
|
67
|
-
throw new ApiError(404, "PROJECT_NOT_FOUND", `Project '${projectId}' was not found`);
|
|
68
|
-
}
|
|
69
|
-
const summary = this.database.getProjectSummary(projectId);
|
|
70
|
-
if (isDue(summary)) void this.refresh(projectId);
|
|
71
|
-
return {
|
|
72
|
-
projectId,
|
|
73
|
-
summary: summary.summary,
|
|
74
|
-
updatedAt: summary.generatedAt,
|
|
75
|
-
refreshing: this.active.has(projectId),
|
|
76
|
-
error: summary.error,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
refresh(projectId) {
|
|
81
|
-
const current = this.active.get(projectId);
|
|
82
|
-
if (current) return current.promise;
|
|
83
|
-
const active = { child: null, promise: null };
|
|
84
|
-
active.promise = this.#generate(projectId, active)
|
|
85
|
-
.finally(() => this.active.delete(projectId));
|
|
86
|
-
this.active.set(projectId, active);
|
|
87
|
-
return active.promise;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async refreshDueProjects() {
|
|
91
|
-
for (const summary of this.database.listProjectSummaries()) {
|
|
92
|
-
if (isDue(summary)) await this.refresh(summary.projectId);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async #generate(projectId, active) {
|
|
97
|
-
try {
|
|
98
|
-
const project = this.database.getProject(projectId);
|
|
99
|
-
if (!project) return;
|
|
100
|
-
const tasks = this.database.listTasks({ projectId, archived: "false" });
|
|
101
|
-
let generatedSummary = "";
|
|
102
|
-
let terminalError = "";
|
|
103
|
-
const { child, completion } = spawnCodexTurn({
|
|
104
|
-
executable: this.codexExecutable,
|
|
105
|
-
args: [
|
|
106
|
-
"exec",
|
|
107
|
-
"--ephemeral",
|
|
108
|
-
"--json",
|
|
109
|
-
"--color",
|
|
110
|
-
"never",
|
|
111
|
-
"--skip-git-repo-check",
|
|
112
|
-
"-C",
|
|
113
|
-
this.workspacePath,
|
|
114
|
-
"-s",
|
|
115
|
-
"read-only",
|
|
116
|
-
"-c",
|
|
117
|
-
'approval_policy="never"',
|
|
118
|
-
"-",
|
|
119
|
-
],
|
|
120
|
-
prompt: buildPrompt(project, tasks),
|
|
121
|
-
env: this.processEnv,
|
|
122
|
-
onRawEvent: (event) => {
|
|
123
|
-
if (
|
|
124
|
-
event.type === "item.completed"
|
|
125
|
-
&& event.item?.type === "agent_message"
|
|
126
|
-
&& typeof event.item.text === "string"
|
|
127
|
-
) {
|
|
128
|
-
generatedSummary = event.item.text.replace(/\s+/g, " ").trim();
|
|
129
|
-
}
|
|
130
|
-
if (event.type === "turn.failed" || event.type === "error") {
|
|
131
|
-
terminalError = String(event.error?.message ?? event.message ?? "Codex 生成失败");
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
active.child = child;
|
|
136
|
-
const result = await completion;
|
|
137
|
-
if (result.exitCode !== 0 || terminalError) {
|
|
138
|
-
throw new Error(terminalError || `Codex 退出码 ${result.exitCode}`);
|
|
139
|
-
}
|
|
140
|
-
if (!generatedSummary) throw new Error("Codex 没有返回项目总结");
|
|
141
|
-
this.database.saveProjectSummary(projectId, generatedSummary);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
if (!this.closed) {
|
|
144
|
-
this.database.saveProjectSummaryError(
|
|
145
|
-
projectId,
|
|
146
|
-
error instanceof Error ? error.message : String(error),
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async close() {
|
|
153
|
-
this.closed = true;
|
|
154
|
-
clearInterval(this.timer);
|
|
155
|
-
const active = [...this.active.values()];
|
|
156
|
-
for (const entry of active) signalProcessTree(entry.child, "SIGTERM");
|
|
157
|
-
await Promise.allSettled(active.map((entry) => entry.promise));
|
|
158
|
-
}
|
|
159
|
-
}
|