@co0ontty/wand 4.55.3-beta.g2c47e41 → 4.56.0-beta.ga71d7db

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.
Files changed (55) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/github-connector.d.ts +22 -0
  3. package/dist/github-connector.js +117 -0
  4. package/dist/missions.js +42 -9
  5. package/dist/server-github-routes.d.ts +10 -0
  6. package/dist/server-github-routes.js +239 -0
  7. package/dist/server-settings-routes.d.ts +2 -0
  8. package/dist/server-settings-routes.js +1 -0
  9. package/dist/server-task-routes.d.ts +4 -0
  10. package/dist/server-task-routes.js +99 -0
  11. package/dist/server-workspace-routes.js +70 -27
  12. package/dist/server.js +25 -0
  13. package/dist/storage.d.ts +42 -0
  14. package/dist/storage.js +223 -0
  15. package/dist/structured-provider-common.js +2 -1
  16. package/dist/task-types.d.ts +19 -0
  17. package/dist/task-types.js +1 -0
  18. package/dist/taskboard-bridge.d.ts +11 -0
  19. package/dist/taskboard-bridge.js +360 -0
  20. package/dist/web-ui/content/scripts.js +123 -127
  21. package/dist/web-ui/content/styles.css +1 -1
  22. package/dist/web-ui/embedded-assets.js +2 -2
  23. package/dist/web-ui/index.js +1 -0
  24. package/package.json +4 -2
  25. package/vendor/codex-taskboard/LICENSE +201 -0
  26. package/vendor/codex-taskboard/server/ai-chat-catalog.mjs +1031 -0
  27. package/vendor/codex-taskboard/server/ai-chat-process.mjs +462 -0
  28. package/vendor/codex-taskboard/server/ai-chat.mjs +1237 -0
  29. package/vendor/codex-taskboard/server/ai-turn-owner.mjs +39 -0
  30. package/vendor/codex-taskboard/server/app.mjs +3211 -0
  31. package/vendor/codex-taskboard/server/cloud-config.mjs +188 -0
  32. package/vendor/codex-taskboard/server/cloud-proxy.mjs +310 -0
  33. package/vendor/codex-taskboard/server/codex-app-server.mjs +376 -0
  34. package/vendor/codex-taskboard/server/codex-slash-commands-0.139.0.json +62 -0
  35. package/vendor/codex-taskboard/server/database.mjs +2845 -0
  36. package/vendor/codex-taskboard/server/index.mjs +41 -0
  37. package/vendor/codex-taskboard/server/jira-config.mjs +181 -0
  38. package/vendor/codex-taskboard/server/jira-integration.mjs +487 -0
  39. package/vendor/codex-taskboard/server/project-summary.mjs +159 -0
  40. package/vendor/codex-taskboard/shared/api-fields.mjs +163 -0
  41. package/vendor/codex-taskboard/shared/codex-environment.mjs +5 -0
  42. package/vendor/codex-taskboard/shared/codex-executable.mjs +77 -0
  43. package/vendor/codex-taskboard/shared/codex-permissions.mjs +9 -0
  44. package/vendor/codex-taskboard/shared/composer-reference.d.mts +19 -0
  45. package/vendor/codex-taskboard/shared/composer-reference.mjs +90 -0
  46. package/vendor/codex-taskboard/shared/domain.mjs +34 -0
  47. package/vendor/codex-taskboard/shared/executable-command.mjs +13 -0
  48. package/vendor/codex-taskboard/shared/process-tree.mjs +27 -0
  49. package/vendor/codex-taskboard/shared/task-input.mjs +308 -0
  50. package/vendor/codex-taskboard/shared/task-records.mjs +199 -0
  51. package/vendor/codex-taskboard/shared/task-relations.mjs +55 -0
  52. package/vendor/codex-taskboard/shared/taskboard-automation.mjs +277 -0
  53. package/vendor/codex-taskboard/skills/manage-taskboard/SKILL.md +45 -0
  54. package/vendor/codex-taskboard/skills/manage-taskboard/agents/openai.yaml +4 -0
  55. package/vendor/codex-taskboard/skills/manage-taskboard/references/cli.md +211 -0
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "2c47e41268f4636a7b0e0360effe84bf6d795631",
3
- "builtAt": "2026-09-08T06:00:48.199Z",
4
- "version": "4.55.3-beta.g2c47e41",
2
+ "commit": "a71d7db07de28e316d86d8a42c23c7a59a8dbace",
3
+ "builtAt": "2026-09-10T01:18:24.234Z",
4
+ "version": "4.56.0-beta.ga71d7db",
5
5
  "channel": "beta"
6
6
  }
@@ -0,0 +1,22 @@
1
+ import type { WandStorage } from "./storage.js";
2
+ export declare const GITHUB_PROVIDER = "github";
3
+ export declare const GITHUB_API_URL = "https://api.github.com";
4
+ export interface GithubConnectorStatus {
5
+ provider: "github";
6
+ connected: boolean;
7
+ apiUrl: string | null;
8
+ username: string | null;
9
+ connectedAt: string | null;
10
+ updatedAt: string | null;
11
+ scopes: string[];
12
+ }
13
+ export interface GithubApiResponse<T> {
14
+ data: T;
15
+ headers: Headers;
16
+ }
17
+ export declare function getGithubApiUrl(storage: WandStorage): string;
18
+ export declare function getGithubConnectorStatus(storage: WandStorage): GithubConnectorStatus;
19
+ export declare function githubRequest<T = unknown>(storage: WandStorage, pathname: string, init?: RequestInit): Promise<GithubApiResponse<T>>;
20
+ export declare function connectGithub(storage: WandStorage, token: string, apiUrl?: string): Promise<GithubConnectorStatus>;
21
+ export declare function githubStatusWithScopes(storage: WandStorage, scopes?: string[]): GithubConnectorStatus;
22
+ export declare function jsonBody(value: unknown): string;
@@ -0,0 +1,117 @@
1
+ export const GITHUB_PROVIDER = "github";
2
+ export const GITHUB_API_URL = "https://api.github.com";
3
+ function normalizeApiUrl(value) {
4
+ const raw = typeof value === "string" && value.trim() ? value.trim() : GITHUB_API_URL;
5
+ let parsed;
6
+ try {
7
+ parsed = new URL(raw);
8
+ }
9
+ catch {
10
+ throw new Error("GitHub API 地址无效。");
11
+ }
12
+ const local = parsed.hostname === "localhost"
13
+ || parsed.hostname === "127.0.0.1"
14
+ || parsed.hostname === "[::1]";
15
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && local)) {
16
+ throw new Error("GitHub API 地址必须使用 HTTPS(本地测试可使用 HTTP)。");
17
+ }
18
+ return parsed.toString().replace(/\/$/, "");
19
+ }
20
+ export function getGithubApiUrl(storage) {
21
+ return normalizeApiUrl(storage.getConnectorMeta(GITHUB_PROVIDER)?.apiUrl || GITHUB_API_URL);
22
+ }
23
+ export function getGithubConnectorStatus(storage) {
24
+ const meta = storage.getConnectorMeta(GITHUB_PROVIDER);
25
+ const token = storage.getConnectorToken(GITHUB_PROVIDER);
26
+ return {
27
+ provider: "github",
28
+ connected: Boolean(token && meta?.connectedAt),
29
+ apiUrl: meta?.apiUrl || null,
30
+ username: meta?.username || null,
31
+ connectedAt: meta?.connectedAt || null,
32
+ updatedAt: meta?.updatedAt || null,
33
+ scopes: [],
34
+ };
35
+ }
36
+ function connectorToken(storage) {
37
+ const token = storage.getConnectorToken(GITHUB_PROVIDER)?.trim();
38
+ if (!token)
39
+ throw new Error("GitHub 尚未连接,请先在设置中保存 Token。");
40
+ return token;
41
+ }
42
+ function apiPath(pathname) {
43
+ if (!pathname.startsWith("/") || pathname.includes("\\") || pathname.includes("..")) {
44
+ throw new Error("GitHub API 路径无效。");
45
+ }
46
+ return pathname;
47
+ }
48
+ export async function githubRequest(storage, pathname, init = {}) {
49
+ const headers = new Headers(init.headers);
50
+ headers.set("Accept", "application/vnd.github+json");
51
+ headers.set("X-GitHub-Api-Version", "2022-11-28");
52
+ headers.set("User-Agent", "wand-github-connector");
53
+ headers.set("Authorization", `Bearer ${connectorToken(storage)}`);
54
+ if (init.body !== undefined && !headers.has("Content-Type"))
55
+ headers.set("Content-Type", "application/json");
56
+ const response = await fetch(`${getGithubApiUrl(storage)}${apiPath(pathname)}`, { ...init, headers });
57
+ const raw = await response.text();
58
+ let data = null;
59
+ try {
60
+ data = raw ? JSON.parse(raw) : null;
61
+ }
62
+ catch {
63
+ data = raw.slice(0, 2000);
64
+ }
65
+ if (!response.ok) {
66
+ const message = data && typeof data === "object" && "message" in data && typeof data.message === "string"
67
+ ? data.message
68
+ : `GitHub API 请求失败(${response.status})。`;
69
+ const error = new Error(message);
70
+ error.status = response.status;
71
+ throw error;
72
+ }
73
+ return { data: data, headers: response.headers };
74
+ }
75
+ export async function connectGithub(storage, token, apiUrl) {
76
+ const normalizedToken = token.trim();
77
+ if (!normalizedToken)
78
+ throw new Error("GitHub Token 不能为空。");
79
+ const targetApiUrl = normalizeApiUrl(apiUrl);
80
+ const previous = storage.getConnectorMeta(GITHUB_PROVIDER);
81
+ const previousToken = storage.getConnectorToken(GITHUB_PROVIDER);
82
+ storage.saveConnector(GITHUB_PROVIDER, { token: normalizedToken, apiUrl: targetApiUrl, markConnected: false });
83
+ try {
84
+ const profile = await githubRequest(storage, "/user");
85
+ const username = typeof profile.data.login === "string" ? profile.data.login : "";
86
+ if (!username)
87
+ throw new Error("GitHub 返回的账号信息无效。");
88
+ const scopes = (profile.headers.get("x-oauth-scopes") || "")
89
+ .split(",").map((scope) => scope.trim()).filter(Boolean);
90
+ const meta = storage.saveConnector(GITHUB_PROVIDER, {
91
+ apiUrl: targetApiUrl,
92
+ username,
93
+ markConnected: true,
94
+ });
95
+ return { ...getGithubConnectorStatus(storage), ...meta, provider: "github", connected: true, scopes };
96
+ }
97
+ catch (error) {
98
+ if (previous?.connectedAt && previousToken) {
99
+ storage.saveConnector(GITHUB_PROVIDER, {
100
+ token: previousToken,
101
+ apiUrl: previous.apiUrl,
102
+ username: previous.username,
103
+ markConnected: true,
104
+ });
105
+ }
106
+ else {
107
+ storage.deleteConnector(GITHUB_PROVIDER);
108
+ }
109
+ throw error;
110
+ }
111
+ }
112
+ export function githubStatusWithScopes(storage, scopes = []) {
113
+ return { ...getGithubConnectorStatus(storage), scopes };
114
+ }
115
+ export function jsonBody(value) {
116
+ return JSON.stringify(value);
117
+ }
package/dist/missions.js CHANGED
@@ -21,11 +21,30 @@ function firstPromptLine(prompt) {
21
21
  const line = prompt.split("\n").map((part) => part.trim()).find(Boolean) || "新任务";
22
22
  return line.length > 72 ? `${line.slice(0, 69)}…` : line;
23
23
  }
24
+ const ACTIVITY_NOISE = /(?:⏵⏵|⎿|bypasspermissions|shift\+tab|esc to interrupt|tip:|worked for|total messages:|ran playwright code)/i;
25
+ function cleanActivityText(value, maxLength) {
26
+ const normalized = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
27
+ if (!normalized || ACTIVITY_NOISE.test(normalized))
28
+ return null;
29
+ return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1).trimEnd()}…` : normalized;
30
+ }
31
+ function cleanActivityTitle(value) {
32
+ const normalized = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
33
+ const match = normalized.match(/^(.*?)\s+(执行中|已完成|失败|等待答复|等待授权)\s*[·:]?\s*(.*)$/);
34
+ if (!match)
35
+ return { title: cleanActivityText(normalized, 72) || "Agent 会话", summary: null };
36
+ return {
37
+ title: cleanActivityText(match[1], 72) || "Agent 会话",
38
+ summary: cleanActivityText(match[3], 160),
39
+ };
40
+ }
24
41
  function sessionSummary(snapshot) {
25
- if (snapshot.description?.trim())
26
- return snapshot.description.trim();
27
- if (snapshot.currentTaskTitle?.trim())
28
- return snapshot.currentTaskTitle.trim();
42
+ const direct = cleanActivityText(snapshot.description?.trim() || "", 160);
43
+ if (direct)
44
+ return direct;
45
+ const taskTitle = cleanActivityText(snapshot.currentTaskTitle?.trim() || "", 160);
46
+ if (taskTitle)
47
+ return taskTitle;
29
48
  const messages = snapshot.messages ?? [];
30
49
  for (let turnIndex = messages.length - 1; turnIndex >= 0; turnIndex--) {
31
50
  const text = messages[turnIndex].content
@@ -33,10 +52,11 @@ function sessionSummary(snapshot) {
33
52
  .map((block) => block.text.trim())
34
53
  .filter(Boolean)
35
54
  .join(" ");
36
- if (text)
37
- return text.length > 240 ? `${text.slice(0, 237)}…` : text;
55
+ const cleaned = cleanActivityText(text, 160);
56
+ if (cleaned)
57
+ return cleaned;
38
58
  }
39
- return snapshot.summary?.trim() || null;
59
+ return cleanActivityText(snapshot.summary?.trim() || "", 160);
40
60
  }
41
61
  function hasUnansweredQuestion(messages) {
42
62
  if (!messages?.length || messages[messages.length - 1]?.role === "user")
@@ -166,12 +186,15 @@ export class Missions {
166
186
  error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
167
187
  updatedAt,
168
188
  });
189
+ const mission = this.storage.getMission(attempt.missionId);
169
190
  this.storage.upsertAgentActivity({
170
191
  sessionId: event.sessionId,
171
192
  missionId: attempt.missionId,
172
193
  attemptId: attempt.id,
173
194
  state,
174
- title: snapshot.title?.trim() || snapshot.summary?.trim() || firstPromptLine(this.storage.getMission(attempt.missionId)?.prompt ?? ""),
195
+ // The session title can contain provider TUI status chrome. Inbox items
196
+ // belong to the mission, so keep their title stable and human-readable.
197
+ title: mission?.title || firstPromptLine(mission?.prompt ?? ""),
175
198
  summary: sessionSummary(snapshot),
176
199
  provider: snapshot.provider ?? attempt.provider,
177
200
  cwd: snapshot.cwd,
@@ -181,7 +204,17 @@ export class Missions {
181
204
  this.refreshMissionStatus(attempt.missionId);
182
205
  }
183
206
  inbox() {
184
- return this.storage.listAgentActivity();
207
+ return this.storage.listAgentActivity().map((item) => {
208
+ const mission = item.missionId ? this.storage.getMission(item.missionId) : null;
209
+ const legacyTitle = cleanActivityTitle(item.title);
210
+ return {
211
+ ...item,
212
+ // Older rows may have been populated from a provider TUI title. Prefer
213
+ // the mission title at read time so existing inbox data is repaired too.
214
+ title: mission?.title || legacyTitle.title,
215
+ summary: cleanActivityText(item.summary ?? "", 160) || legacyTitle.summary,
216
+ };
217
+ });
185
218
  }
186
219
  markInboxRead(sessionId) {
187
220
  this.storage.markAgentActivityRead(sessionId);
@@ -0,0 +1,10 @@
1
+ import type { Express, RequestHandler } from "express";
2
+ import type { WandStorage } from "./storage.js";
3
+ import type { SessionRegistry } from "./session-registry.js";
4
+ interface GithubRouteDependencies {
5
+ storage: WandStorage;
6
+ requireAdmin: RequestHandler;
7
+ sessions?: SessionRegistry;
8
+ }
9
+ export declare function registerGithubRoutes(app: Express, deps: GithubRouteDependencies): void;
10
+ export {};
@@ -0,0 +1,239 @@
1
+ import { asyncRoute } from "./express-async.js";
2
+ import { connectGithub, githubRequest, githubStatusWithScopes, jsonBody, } from "./github-connector.js";
3
+ import { getErrorMessage } from "./error-utils.js";
4
+ function stringQuery(value, fallback = "") {
5
+ return typeof value === "string" ? value.trim() : fallback;
6
+ }
7
+ function integerQuery(value, fallback, min, max) {
8
+ const parsed = Number(stringQuery(value));
9
+ return Number.isInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;
10
+ }
11
+ function repoPath(req) {
12
+ const owner = req.params.owner?.trim();
13
+ const repo = req.params.repo?.trim();
14
+ if (!owner || !repo || !/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(repo)) {
15
+ throw new Error("仓库名称无效。");
16
+ }
17
+ return `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
18
+ }
19
+ function issueNumber(req) {
20
+ const number = Number(req.params.number);
21
+ if (!Number.isInteger(number) || number < 1)
22
+ throw new Error("Issue 或 PR 编号无效。");
23
+ return number;
24
+ }
25
+ function validateObjectBody(body) {
26
+ if (!body || typeof body !== "object" || Array.isArray(body))
27
+ throw new Error("请求体必须是对象。");
28
+ return body;
29
+ }
30
+ function requiredString(body, key) {
31
+ const value = typeof body[key] === "string" ? body[key].trim() : "";
32
+ if (!value)
33
+ throw new Error(`${key} 不能为空。`);
34
+ return value;
35
+ }
36
+ function publicError(error) {
37
+ const status = error instanceof Error && "status" in error && typeof error.status === "number"
38
+ ? error.status
39
+ : 400;
40
+ return { status: status >= 400 && status < 600 ? status : 500, message: getErrorMessage(error, "GitHub 操作失败。") };
41
+ }
42
+ export function registerGithubRoutes(app, deps) {
43
+ const { storage, requireAdmin, sessions } = deps;
44
+ app.get("/api/connectors/github", requireAdmin, (_req, res) => {
45
+ res.set("Cache-Control", "no-store");
46
+ res.json(githubStatusWithScopes(storage));
47
+ });
48
+ app.post("/api/connectors/github", requireAdmin, asyncRoute(async (req, res) => {
49
+ try {
50
+ const body = validateObjectBody(req.body);
51
+ const token = requiredString(body, "token");
52
+ const apiUrl = typeof body.apiUrl === "string" ? body.apiUrl : undefined;
53
+ res.set("Cache-Control", "no-store");
54
+ res.json(await connectGithub(storage, token, apiUrl));
55
+ }
56
+ catch (error) {
57
+ const result = publicError(error);
58
+ res.status(result.status === 401 || result.status === 403 ? 400 : result.status).json({ error: result.message });
59
+ }
60
+ }));
61
+ app.delete("/api/connectors/github", requireAdmin, (_req, res) => {
62
+ storage.deleteConnector("github");
63
+ res.json({ ok: true, connected: false });
64
+ });
65
+ app.get("/api/github/repos/:owner/:repo/issues/:number/bindings", requireAdmin, (req, res) => {
66
+ try {
67
+ const number = issueNumber(req);
68
+ const bindings = storage.listGithubIssueBindings(req.params.owner, req.params.repo, number);
69
+ res.json({ bindings });
70
+ }
71
+ catch (error) {
72
+ const result = publicError(error);
73
+ res.status(result.status).json({ error: result.message });
74
+ }
75
+ });
76
+ app.post("/api/github/repos/:owner/:repo/issues/:number/bindings", requireAdmin, (req, res) => {
77
+ try {
78
+ const number = issueNumber(req);
79
+ const body = validateObjectBody(req.body);
80
+ const sessionId = requiredString(body, "sessionId");
81
+ if (sessions && !sessions.get(sessionId))
82
+ throw new Error("未找到该会话。");
83
+ storage.bindGithubIssueSession(req.params.owner, req.params.repo, number, sessionId);
84
+ res.status(201).json({ ok: true, sessionId });
85
+ }
86
+ catch (error) {
87
+ const result = publicError(error);
88
+ res.status(result.status).json({ error: result.message });
89
+ }
90
+ });
91
+ app.delete("/api/github/repos/:owner/:repo/issues/:number/bindings/:sessionId", requireAdmin, (req, res) => {
92
+ try {
93
+ storage.unbindGithubIssueSession(req.params.owner, req.params.repo, issueNumber(req), req.params.sessionId);
94
+ res.json({ ok: true });
95
+ }
96
+ catch (error) {
97
+ const result = publicError(error);
98
+ res.status(result.status).json({ error: result.message });
99
+ }
100
+ });
101
+ app.get("/api/github/user", requireAdmin, asyncRoute(async (_req, res) => {
102
+ try {
103
+ res.json((await githubRequest(storage, "/user")).data);
104
+ }
105
+ catch (error) {
106
+ const result = publicError(error);
107
+ res.status(result.status).json({ error: result.message });
108
+ }
109
+ }));
110
+ app.get("/api/github/repos", requireAdmin, asyncRoute(async (req, res) => {
111
+ try {
112
+ const query = new URLSearchParams();
113
+ const visibility = stringQuery(req.query.visibility);
114
+ const affiliation = stringQuery(req.query.affiliation);
115
+ const type = stringQuery(req.query.type);
116
+ if (visibility)
117
+ query.set("visibility", visibility);
118
+ if (affiliation)
119
+ query.set("affiliation", affiliation);
120
+ if (type)
121
+ query.set("type", type);
122
+ query.set("sort", stringQuery(req.query.sort, "updated"));
123
+ query.set("direction", stringQuery(req.query.direction, "desc"));
124
+ query.set("per_page", String(integerQuery(req.query.per_page, 30, 1, 100)));
125
+ query.set("page", String(integerQuery(req.query.page, 1, 1, 10_000)));
126
+ const suffix = query.toString();
127
+ res.json((await githubRequest(storage, `/user/repos?${suffix}`)).data);
128
+ }
129
+ catch (error) {
130
+ const result = publicError(error);
131
+ res.status(result.status).json({ error: result.message });
132
+ }
133
+ }));
134
+ app.get("/api/github/repos/:owner/:repo", requireAdmin, asyncRoute(async (req, res) => {
135
+ try {
136
+ res.json((await githubRequest(storage, repoPath(req))).data);
137
+ }
138
+ catch (error) {
139
+ const result = publicError(error);
140
+ res.status(result.status).json({ error: result.message });
141
+ }
142
+ }));
143
+ app.get("/api/github/repos/:owner/:repo/issues", requireAdmin, asyncRoute(async (req, res) => {
144
+ try {
145
+ const query = new URLSearchParams();
146
+ query.set("state", stringQuery(req.query.state, "open"));
147
+ if (stringQuery(req.query.labels))
148
+ query.set("labels", stringQuery(req.query.labels));
149
+ if (stringQuery(req.query.sort))
150
+ query.set("sort", stringQuery(req.query.sort));
151
+ if (stringQuery(req.query.direction))
152
+ query.set("direction", stringQuery(req.query.direction));
153
+ query.set("per_page", String(integerQuery(req.query.per_page, 30, 1, 100)));
154
+ query.set("page", String(integerQuery(req.query.page, 1, 1, 10_000)));
155
+ res.json((await githubRequest(storage, `${repoPath(req)}/issues?${query}`)).data);
156
+ }
157
+ catch (error) {
158
+ const result = publicError(error);
159
+ res.status(result.status).json({ error: result.message });
160
+ }
161
+ }));
162
+ app.post("/api/github/repos/:owner/:repo/issues", requireAdmin, asyncRoute(async (req, res) => {
163
+ try {
164
+ const body = validateObjectBody(req.body);
165
+ const payload = { title: requiredString(body, "title") };
166
+ for (const key of ["body", "labels", "assignees", "milestone"]) {
167
+ if (body[key] !== undefined)
168
+ payload[key] = body[key];
169
+ }
170
+ res.status(201).json((await githubRequest(storage, `${repoPath(req)}/issues`, { method: "POST", body: jsonBody(payload) })).data);
171
+ }
172
+ catch (error) {
173
+ const result = publicError(error);
174
+ res.status(result.status).json({ error: result.message });
175
+ }
176
+ }));
177
+ app.patch("/api/github/repos/:owner/:repo/issues/:number", requireAdmin, asyncRoute(async (req, res) => {
178
+ try {
179
+ const body = validateObjectBody(req.body);
180
+ const payload = {};
181
+ for (const key of ["title", "body", "state", "state_reason", "labels", "assignees", "milestone"]) {
182
+ if (body[key] !== undefined)
183
+ payload[key] = body[key];
184
+ }
185
+ if (!Object.keys(payload).length)
186
+ throw new Error("没有可更新的 Issue 字段。");
187
+ res.json((await githubRequest(storage, `${repoPath(req)}/issues/${issueNumber(req)}`, { method: "PATCH", body: jsonBody(payload) })).data);
188
+ }
189
+ catch (error) {
190
+ const result = publicError(error);
191
+ res.status(result.status).json({ error: result.message });
192
+ }
193
+ }));
194
+ app.post("/api/github/repos/:owner/:repo/issues/:number/comments", requireAdmin, asyncRoute(async (req, res) => {
195
+ try {
196
+ const body = validateObjectBody(req.body);
197
+ const payload = { body: requiredString(body, "body") };
198
+ res.status(201).json((await githubRequest(storage, `${repoPath(req)}/issues/${issueNumber(req)}/comments`, { method: "POST", body: jsonBody(payload) })).data);
199
+ }
200
+ catch (error) {
201
+ const result = publicError(error);
202
+ res.status(result.status).json({ error: result.message });
203
+ }
204
+ }));
205
+ app.get("/api/github/repos/:owner/:repo/pulls", requireAdmin, asyncRoute(async (req, res) => {
206
+ try {
207
+ const query = new URLSearchParams();
208
+ query.set("state", stringQuery(req.query.state, "open"));
209
+ query.set("sort", stringQuery(req.query.sort, "created"));
210
+ query.set("direction", stringQuery(req.query.direction, "desc"));
211
+ query.set("per_page", String(integerQuery(req.query.per_page, 30, 1, 100)));
212
+ query.set("page", String(integerQuery(req.query.page, 1, 1, 10_000)));
213
+ res.json((await githubRequest(storage, `${repoPath(req)}/pulls?${query}`)).data);
214
+ }
215
+ catch (error) {
216
+ const result = publicError(error);
217
+ res.status(result.status).json({ error: result.message });
218
+ }
219
+ }));
220
+ app.post("/api/github/repos/:owner/:repo/pulls", requireAdmin, asyncRoute(async (req, res) => {
221
+ try {
222
+ const body = validateObjectBody(req.body);
223
+ const payload = {
224
+ title: requiredString(body, "title"),
225
+ head: requiredString(body, "head"),
226
+ base: requiredString(body, "base"),
227
+ };
228
+ for (const key of ["body", "draft", "maintainer_can_modify"]) {
229
+ if (body[key] !== undefined)
230
+ payload[key] = body[key];
231
+ }
232
+ res.status(201).json((await githubRequest(storage, `${repoPath(req)}/pulls`, { method: "POST", body: jsonBody(payload) })).data);
233
+ }
234
+ catch (error) {
235
+ const result = publicError(error);
236
+ res.status(result.status).json({ error: result.message });
237
+ }
238
+ }));
239
+ }
@@ -3,6 +3,7 @@ import type { ModelCatalogService } from "./models.js";
3
3
  import { type RuntimeConfigState } from "./runtime-config.js";
4
4
  import type { WandStorage } from "./storage.js";
5
5
  import type { WandConfig } from "./types.js";
6
+ import type { GithubConnectorStatus } from "./github-connector.js";
6
7
  interface SettingsDistributionPayload {
7
8
  androidApk: Record<string, unknown>;
8
9
  macosDmg: Record<string, unknown>;
@@ -34,6 +35,7 @@ export interface ServerSettingsRoutesDependencies {
34
35
  } | null;
35
36
  getUpdateChannel(): "stable" | "beta";
36
37
  getDistributionSettings(): Promise<SettingsDistributionPayload>;
38
+ getGithubConnector(): GithubConnectorStatus;
37
39
  modelCatalog: ModelCatalogService;
38
40
  resolveAppConnectCode(req: Request): {
39
41
  code: string;
@@ -91,6 +91,7 @@ export function registerSettingsRoutes(app, deps) {
91
91
  activeConfig: publicConfig(config),
92
92
  restartRequired: runtimeConfig.hasPendingRestart(),
93
93
  hasCert: existsSync(path.join(configDir, "server.key")) && existsSync(path.join(configDir, "server.crt")),
94
+ githubConnector: deps.getGithubConnector(),
94
95
  updateAvailable: cachedUpdate?.updateAvailable ?? false,
95
96
  latestVersion: cachedUpdate?.latest ?? null,
96
97
  updateChannel: deps.getUpdateChannel(),
@@ -0,0 +1,4 @@
1
+ import type { Express, RequestHandler } from "express";
2
+ import type { WandStorage } from "./storage.js";
3
+ import type { SessionRegistry } from "./session-registry.js";
4
+ export declare function registerTaskRoutes(app: Express, storage: WandStorage, sessions?: SessionRegistry, requireAdmin?: RequestHandler): void;
@@ -0,0 +1,99 @@
1
+ import { asyncRoute } from "./express-async.js";
2
+ import { getErrorMessage } from "./error-utils.js";
3
+ const STATUSES = new Set(["todo", "doing", "done"]);
4
+ const PRIORITIES = new Set(["none", "low", "medium", "high", "urgent"]);
5
+ function dateValue(value) { if (value === undefined)
6
+ return undefined; if (value === null || value === "")
7
+ return null; if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value.trim()))
8
+ throw new Error("截止日期必须使用 YYYY-MM-DD 格式。"); return value.trim(); }
9
+ function bodyObject(value) { if (!value || typeof value !== "object" || Array.isArray(value))
10
+ throw new Error("请求体必须是对象。"); return value; }
11
+ function text(value) { return typeof value === "string" ? value.trim() : ""; }
12
+ function taskDto(storage, task) { if (!task)
13
+ return null; return { ...task, sessionIds: storage.listWandTaskSessionIds(task.id) }; }
14
+ export function registerTaskRoutes(app, storage, sessions, requireAdmin) {
15
+ const guard = requireAdmin ?? ((_req, _res, next) => next());
16
+ app.get("/api/wand-tasks", guard, (req, res) => res.json(storage.listWandTasks(typeof req.query.workspaceId === "string" ? req.query.workspaceId : undefined).map((task) => taskDto(storage, task))));
17
+ app.post("/api/wand-tasks", guard, (req, res) => { try {
18
+ const body = bodyObject(req.body);
19
+ const title = text(body.title);
20
+ if (!title)
21
+ throw new Error("任务标题不能为空。");
22
+ const workspaceId = body.workspaceId === null ? null : text(body.workspaceId) || null;
23
+ if (workspaceId && !storage.getWorkspace(workspaceId))
24
+ throw new Error("项目不存在。");
25
+ const status = STATUSES.has(body.status) ? body.status : "todo";
26
+ const priority = PRIORITIES.has(body.priority) ? body.priority : "none";
27
+ const labels = Array.isArray(body.labels) ? body.labels.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean).slice(0, 20) : [];
28
+ const task = storage.createWandTask({ workspaceId, title, description: text(body.description), status, priority, labels, dueDate: dateValue(body.dueDate) ?? null });
29
+ res.status(201).json(taskDto(storage, task));
30
+ }
31
+ catch (error) {
32
+ res.status(400).json({ error: getErrorMessage(error, "无法创建任务。") });
33
+ } });
34
+ app.patch("/api/wand-tasks/:id", guard, (req, res) => { try {
35
+ const body = bodyObject(req.body);
36
+ const patch = {};
37
+ if (body.title !== undefined) {
38
+ const value = text(body.title);
39
+ if (!value)
40
+ throw new Error("任务标题不能为空。");
41
+ patch.title = value;
42
+ }
43
+ if (body.description !== undefined)
44
+ patch.description = text(body.description);
45
+ if (body.status !== undefined) {
46
+ if (!STATUSES.has(body.status))
47
+ throw new Error("任务状态无效。");
48
+ patch.status = body.status;
49
+ }
50
+ if (body.priority !== undefined) {
51
+ if (!PRIORITIES.has(body.priority))
52
+ throw new Error("任务优先级无效。");
53
+ patch.priority = body.priority;
54
+ }
55
+ if (body.labels !== undefined)
56
+ patch.labels = Array.isArray(body.labels) ? body.labels.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean).slice(0, 20) : [];
57
+ if (body.dueDate !== undefined)
58
+ patch.dueDate = dateValue(body.dueDate) ?? null;
59
+ if (body.workspaceId !== undefined) {
60
+ const workspaceId = body.workspaceId === null ? null : text(body.workspaceId);
61
+ if (workspaceId && !storage.getWorkspace(workspaceId))
62
+ throw new Error("项目不存在。");
63
+ patch.workspaceId = workspaceId || null;
64
+ }
65
+ if (body.sortOrder !== undefined && Number.isFinite(Number(body.sortOrder)))
66
+ patch.sortOrder = Math.floor(Number(body.sortOrder));
67
+ const task = storage.updateWandTask(req.params.id, patch);
68
+ if (!task) {
69
+ res.status(404).json({ error: "未找到该任务。" });
70
+ return;
71
+ }
72
+ res.json(taskDto(storage, task));
73
+ }
74
+ catch (error) {
75
+ res.status(400).json({ error: getErrorMessage(error, "无法更新任务。") });
76
+ } });
77
+ app.delete("/api/wand-tasks/:id", guard, (req, res) => { if (!storage.getWandTask(req.params.id)) {
78
+ res.status(404).json({ error: "未找到该任务。" });
79
+ return;
80
+ } storage.deleteWandTask(req.params.id); res.json({ ok: true }); });
81
+ app.post("/api/wand-tasks/:id/sessions", guard, (req, res) => { try {
82
+ const body = bodyObject(req.body);
83
+ const sessionId = text(body.sessionId);
84
+ if (!sessionId)
85
+ throw new Error("缺少 sessionId。");
86
+ if (sessions && !sessions.get(sessionId))
87
+ throw new Error("未找到该会话。");
88
+ storage.bindWandTaskSession(req.params.id, sessionId);
89
+ res.status(201).json(taskDto(storage, storage.getWandTask(req.params.id)));
90
+ }
91
+ catch (error) {
92
+ res.status(400).json({ error: getErrorMessage(error, "无法绑定会话。") });
93
+ } });
94
+ app.delete("/api/wand-tasks/:id/sessions/:sessionId", guard, (req, res) => { storage.unbindWandTaskSession(req.params.id, req.params.sessionId); res.json({ ok: true }); });
95
+ app.get("/api/wand-tasks/:id", guard, asyncRoute(async (req, res) => { const task = taskDto(storage, storage.getWandTask(req.params.id)); if (!task) {
96
+ res.status(404).json({ error: "未找到该任务。" });
97
+ return;
98
+ } res.json(task); }));
99
+ }