@co0ontty/wand 4.55.3 → 4.56.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/github-connector.d.ts +22 -0
- package/dist/github-connector.js +117 -0
- package/dist/missions.js +42 -9
- package/dist/server-github-routes.d.ts +10 -0
- package/dist/server-github-routes.js +239 -0
- package/dist/server-settings-routes.d.ts +2 -0
- package/dist/server-settings-routes.js +1 -0
- package/dist/server-task-routes.d.ts +4 -0
- package/dist/server-task-routes.js +99 -0
- package/dist/server-workspace-routes.js +70 -27
- package/dist/server.js +25 -0
- package/dist/storage.d.ts +42 -0
- package/dist/storage.js +223 -0
- package/dist/structured-provider-common.js +2 -1
- package/dist/task-types.d.ts +19 -0
- package/dist/task-types.js +1 -0
- package/dist/taskboard-bridge.d.ts +11 -0
- package/dist/taskboard-bridge.js +360 -0
- package/dist/web-ui/content/scripts.js +123 -127
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/index.js +1 -0
- package/package.json +4 -2
- package/vendor/codex-taskboard/LICENSE +201 -0
- package/vendor/codex-taskboard/server/ai-chat-catalog.mjs +1031 -0
- package/vendor/codex-taskboard/server/ai-chat-process.mjs +462 -0
- package/vendor/codex-taskboard/server/ai-chat.mjs +1237 -0
- package/vendor/codex-taskboard/server/ai-turn-owner.mjs +39 -0
- package/vendor/codex-taskboard/server/app.mjs +3211 -0
- package/vendor/codex-taskboard/server/cloud-config.mjs +188 -0
- package/vendor/codex-taskboard/server/cloud-proxy.mjs +310 -0
- package/vendor/codex-taskboard/server/codex-app-server.mjs +376 -0
- package/vendor/codex-taskboard/server/codex-slash-commands-0.139.0.json +62 -0
- package/vendor/codex-taskboard/server/database.mjs +2845 -0
- package/vendor/codex-taskboard/server/index.mjs +41 -0
- package/vendor/codex-taskboard/server/jira-config.mjs +181 -0
- package/vendor/codex-taskboard/server/jira-integration.mjs +487 -0
- package/vendor/codex-taskboard/server/project-summary.mjs +159 -0
- package/vendor/codex-taskboard/shared/api-fields.mjs +163 -0
- package/vendor/codex-taskboard/shared/codex-environment.mjs +5 -0
- package/vendor/codex-taskboard/shared/codex-executable.mjs +77 -0
- package/vendor/codex-taskboard/shared/codex-permissions.mjs +9 -0
- package/vendor/codex-taskboard/shared/composer-reference.d.mts +19 -0
- package/vendor/codex-taskboard/shared/composer-reference.mjs +90 -0
- package/vendor/codex-taskboard/shared/domain.mjs +34 -0
- package/vendor/codex-taskboard/shared/executable-command.mjs +13 -0
- package/vendor/codex-taskboard/shared/process-tree.mjs +27 -0
- package/vendor/codex-taskboard/shared/task-input.mjs +308 -0
- package/vendor/codex-taskboard/shared/task-records.mjs +199 -0
- package/vendor/codex-taskboard/shared/task-relations.mjs +55 -0
- package/vendor/codex-taskboard/shared/taskboard-automation.mjs +277 -0
- package/vendor/codex-taskboard/skills/manage-taskboard/SKILL.md +45 -0
- package/vendor/codex-taskboard/skills/manage-taskboard/agents/openai.yaml +4 -0
- package/vendor/codex-taskboard/skills/manage-taskboard/references/cli.md +211 -0
|
@@ -8,7 +8,7 @@ import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSess
|
|
|
8
8
|
import { collectSessionTopicBlocklist } from "./session-topic.js";
|
|
9
9
|
import { resolveSessionDisplayTitle } from "./session-transport.js";
|
|
10
10
|
import { GLOBAL_WORKSPACE_ID } from "./types.js";
|
|
11
|
-
import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, } from "./workspace-binding.js";
|
|
11
|
+
import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, } from "./workspace-binding.js";
|
|
12
12
|
const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
|
|
13
13
|
function workspaceSessionTitle(session, names = {}) {
|
|
14
14
|
return resolveSessionDisplayTitle(session, collectSessionTopicBlocklist({
|
|
@@ -71,12 +71,15 @@ function taskRuntimeCwd(task, workspace) {
|
|
|
71
71
|
}
|
|
72
72
|
function createTaskForWorkspace(storage, workspace, body) {
|
|
73
73
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
74
|
-
|
|
75
|
-
throw new Error("请输入任务名称。");
|
|
74
|
+
const taskName = name || "未命名任务";
|
|
76
75
|
let mountedCwd;
|
|
77
76
|
if (body.cwd !== undefined && body.cwd !== null && String(body.cwd).trim()) {
|
|
78
77
|
mountedCwd = resolveWorkspaceCwd(body.cwd);
|
|
79
78
|
}
|
|
79
|
+
if (mountedCwd && !isGlobalWorkspace(workspace)
|
|
80
|
+
&& normalizeProjectCwd(mountedCwd) !== normalizeProjectCwd(workspace.cwd)) {
|
|
81
|
+
throw new Error("任务目录与所属项目不一致,请先选择对应项目,或创建独立任务。");
|
|
82
|
+
}
|
|
80
83
|
const baseRef = typeof body.baseRef === "string" && body.baseRef.trim() ? body.baseRef.trim() : undefined;
|
|
81
84
|
const runCwd = mountedCwd ?? workspace.cwd;
|
|
82
85
|
const wantWorktree = body.worktree === true || (body.worktree !== false && !isGlobalWorkspace(workspace));
|
|
@@ -87,7 +90,7 @@ function createTaskForWorkspace(storage, workspace, body) {
|
|
|
87
90
|
const setup = prepareSessionWorktree({
|
|
88
91
|
cwd: runCwd,
|
|
89
92
|
sessionId: crypto.randomUUID(),
|
|
90
|
-
spec: { taskName
|
|
93
|
+
spec: { taskName, baseRef },
|
|
91
94
|
});
|
|
92
95
|
worktree = setup.worktree;
|
|
93
96
|
}
|
|
@@ -98,9 +101,9 @@ function createTaskForWorkspace(storage, workspace, body) {
|
|
|
98
101
|
const storedCwd = mountedCwd && mountedCwd !== workspace.cwd ? mountedCwd : undefined;
|
|
99
102
|
const task = storage.createWorkspaceTask({
|
|
100
103
|
workspaceId: workspace.id,
|
|
101
|
-
name,
|
|
102
|
-
worktree,
|
|
104
|
+
name: taskName,
|
|
103
105
|
cwd: storedCwd,
|
|
106
|
+
worktree,
|
|
104
107
|
});
|
|
105
108
|
return {
|
|
106
109
|
task,
|
|
@@ -392,27 +395,61 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
392
395
|
workspaceName: global ? "全局" : workspace.name,
|
|
393
396
|
workspaceCwd: workspace.cwd,
|
|
394
397
|
...(global ? { global: true } : {}),
|
|
395
|
-
tasks:
|
|
396
|
-
.slice(0, taskLimit ?? undefined)
|
|
397
|
-
.map((task) => {
|
|
398
|
-
const allSessions = storage.listSessionsByWorkspaceTask(task.id);
|
|
399
|
-
const sessions = allSessions
|
|
400
|
-
.slice(0, sessionLimit ?? undefined)
|
|
401
|
-
.map((session) => ({
|
|
402
|
-
...summarize(session, { taskName: task.name, workspaceName: global ? undefined : workspace.name }),
|
|
403
|
-
workspaceTaskId: task.id,
|
|
404
|
-
}));
|
|
405
|
-
return {
|
|
406
|
-
...task,
|
|
407
|
-
cwd: taskRuntimeCwd(task, workspace),
|
|
408
|
-
isolated: task.worktree !== null,
|
|
409
|
-
sessions,
|
|
410
|
-
totalSessions: allSessions.length,
|
|
411
|
-
};
|
|
412
|
-
}),
|
|
398
|
+
tasks: [],
|
|
413
399
|
standaloneSessions: [],
|
|
414
400
|
});
|
|
415
401
|
}
|
|
402
|
+
const syntheticGroupForCwd = (cwd) => {
|
|
403
|
+
const normalized = normalizeProjectCwd(cwd);
|
|
404
|
+
const existingProject = [...groups.values()].find((candidate) => !candidate.synthetic
|
|
405
|
+
&& !candidate.global && normalizeProjectCwd(candidate.workspaceCwd) === normalized);
|
|
406
|
+
if (existingProject)
|
|
407
|
+
return existingProject;
|
|
408
|
+
const id = `cwd:${normalized}`;
|
|
409
|
+
const existing = groups.get(id);
|
|
410
|
+
if (existing)
|
|
411
|
+
return existing;
|
|
412
|
+
const created = {
|
|
413
|
+
workspaceId: id,
|
|
414
|
+
workspaceName: normalized.split("/").filter(Boolean).at(-1) || normalized,
|
|
415
|
+
workspaceCwd: normalized,
|
|
416
|
+
synthetic: true,
|
|
417
|
+
tasks: [],
|
|
418
|
+
standaloneSessions: [],
|
|
419
|
+
};
|
|
420
|
+
groups.set(id, created);
|
|
421
|
+
return created;
|
|
422
|
+
};
|
|
423
|
+
const appendTask = (target, workspace, task) => {
|
|
424
|
+
const allSessions = storage.listSessionsByWorkspaceTask(task.id);
|
|
425
|
+
const sessions = allSessions
|
|
426
|
+
.slice(0, sessionLimit ?? undefined)
|
|
427
|
+
.map((session) => ({
|
|
428
|
+
...summarize(session, { taskName: task.name, workspaceName: isGlobalWorkspace(workspace) ? undefined : workspace.name }),
|
|
429
|
+
workspaceTaskId: task.id,
|
|
430
|
+
}));
|
|
431
|
+
target.tasks.push({
|
|
432
|
+
...task,
|
|
433
|
+
cwd: taskRuntimeCwd(task, workspace),
|
|
434
|
+
isolated: task.worktree !== null,
|
|
435
|
+
sessions,
|
|
436
|
+
totalSessions: allSessions.length,
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
for (const workspace of visibleWorkspaces) {
|
|
440
|
+
const base = groups.get(workspace.id);
|
|
441
|
+
if (!base)
|
|
442
|
+
continue;
|
|
443
|
+
const global = isGlobalWorkspace(workspace);
|
|
444
|
+
for (const task of storage.listWorkspaceTasks(workspace.id).slice(0, taskLimit ?? undefined)) {
|
|
445
|
+
const taskCwd = taskRuntimeCwd(task, workspace);
|
|
446
|
+
const target = global && taskCwd
|
|
447
|
+
&& normalizeProjectCwd(taskCwd) !== normalizeProjectCwd(workspace.cwd)
|
|
448
|
+
? syntheticGroupForCwd(taskCwd)
|
|
449
|
+
: base;
|
|
450
|
+
appendTask(target, workspace, task);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
416
453
|
const taskBoundSessionIds = new Set();
|
|
417
454
|
for (const workspace of workspaces) {
|
|
418
455
|
for (const task of storage.listWorkspaceTasks(workspace.id)) {
|
|
@@ -424,10 +461,16 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
424
461
|
if (taskBoundSessionIds.has(session.id))
|
|
425
462
|
continue;
|
|
426
463
|
const direct = session.workspaceId ? groups.get(session.workspaceId) : undefined;
|
|
427
|
-
|
|
428
|
-
|
|
464
|
+
const resolved = projectCwdForSession(session);
|
|
465
|
+
// 持久化的 workspaceId 可能来自旧目录或目录迁移;path 与项目目录不一致时,
|
|
466
|
+
// 必须按当前实际目录归类,避免不同项目的会话混入同一组。
|
|
467
|
+
let group = direct && !direct.synthetic
|
|
468
|
+
&& normalizeProjectCwd(direct.workspaceCwd) === resolved
|
|
469
|
+
? direct
|
|
470
|
+
: undefined;
|
|
429
471
|
if (!group && resolved) {
|
|
430
|
-
const cwdMatches = [...groups.values()].filter((candidate) => !candidate.synthetic
|
|
472
|
+
const cwdMatches = [...groups.values()].filter((candidate) => !candidate.synthetic
|
|
473
|
+
&& normalizeProjectCwd(candidate.workspaceCwd) === resolved);
|
|
431
474
|
group = cwdMatches.find((candidate) => !candidate.global) ?? cwdMatches[0];
|
|
432
475
|
}
|
|
433
476
|
// 过滤模式下不创建合成组:不属于目标目录的会话直接排除。
|
package/dist/server.js
CHANGED
|
@@ -22,6 +22,10 @@ import { resolveSessionAiContext, resolveSystemAiContext } from "./session-ai-co
|
|
|
22
22
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
23
23
|
import { recordRecentPath, registerFileRoutes } from "./server-file-routes.js";
|
|
24
24
|
import { registerSettingsRoutes } from "./server-settings-routes.js";
|
|
25
|
+
import { registerGithubRoutes } from "./server-github-routes.js";
|
|
26
|
+
import { startTaskboardBridge } from "./taskboard-bridge.js";
|
|
27
|
+
import { registerTaskRoutes } from "./server-task-routes.js";
|
|
28
|
+
import { getGithubConnectorStatus } from "./github-connector.js";
|
|
25
29
|
import { registerMissionRoutes } from "./server-mission-routes.js";
|
|
26
30
|
import { Missions } from "./missions.js";
|
|
27
31
|
import { refreshProviderCliUpdateState, registerAdminUpdateRoutes, registerPublicUpdateRoutes, ServerUpdateState, } from "./server-update-routes.js";
|
|
@@ -554,6 +558,20 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
554
558
|
const requireSessions = buildRequireScope("sessions");
|
|
555
559
|
const requireFiles = buildRequireScope("files");
|
|
556
560
|
const requirePasswordVault = buildRequireScope("password-vault");
|
|
561
|
+
const taskboardBridge = await startTaskboardBridge(configDir, storage, sessionRegistry, structuredSessions, config.defaultCwd);
|
|
562
|
+
app.use("/taskboard", (req, res, next) => {
|
|
563
|
+
// Taskboard's immutable Vite assets are safe to serve without the Wand
|
|
564
|
+
// session cookie. This matters behind HTTPS reverse proxies where iframe
|
|
565
|
+
// subresource requests may not carry the parent authentication cookie.
|
|
566
|
+
const staticAsset = req.path.startsWith("/assets/")
|
|
567
|
+
|| /^\/[^/]+\.(?:css|js|map|svg|png|jpg|jpeg|webp|ico)$/.test(req.path);
|
|
568
|
+
if (staticAsset) {
|
|
569
|
+
taskboardBridge.handler(req, res, next);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
requireAuth(req, res, next);
|
|
573
|
+
});
|
|
574
|
+
app.use("/taskboard", taskboardBridge.handler);
|
|
557
575
|
// Route-specific parsers must run before the global parser. Once body-parser
|
|
558
576
|
// has consumed a request, a later express.json() cannot tighten or widen it.
|
|
559
577
|
app.use("/api/optimize-prompt", express.json({ limit: "256kb" }));
|
|
@@ -953,6 +971,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
953
971
|
getCachedUpdateInfo: () => cachedUpdateInfo,
|
|
954
972
|
getUpdateChannel,
|
|
955
973
|
getDistributionSettings,
|
|
974
|
+
getGithubConnector: () => getGithubConnectorStatus(storage),
|
|
956
975
|
modelCatalog,
|
|
957
976
|
resolveAppConnectCode: (req) => {
|
|
958
977
|
const effectivePassword = getEffectivePassword(storage, config);
|
|
@@ -964,6 +983,8 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
964
983
|
return { code: encodeConnectCode(serverUrl, token), url: serverUrl };
|
|
965
984
|
},
|
|
966
985
|
});
|
|
986
|
+
registerGithubRoutes(app, { storage, requireAdmin, sessions: sessionRegistry });
|
|
987
|
+
registerTaskRoutes(app, storage, sessionRegistry, requireAdmin);
|
|
967
988
|
registerAdminUpdateRoutes(app, {
|
|
968
989
|
storage,
|
|
969
990
|
config,
|
|
@@ -1565,6 +1586,10 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
1565
1586
|
wss.close();
|
|
1566
1587
|
}
|
|
1567
1588
|
catch { /* ignore */ }
|
|
1589
|
+
try {
|
|
1590
|
+
await taskboardBridge.close();
|
|
1591
|
+
}
|
|
1592
|
+
catch { /* best-effort shutdown */ }
|
|
1568
1593
|
try {
|
|
1569
1594
|
await serverClosed;
|
|
1570
1595
|
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { SessionSnapshot, ConversationTurn, StructuredSessionState, Workspace, LayoutNode, TaskWindowLayout, WorkspaceDefaultProvider, WorkspaceKind, WorkspaceTask, WorkspaceTaskWorktree, WorkspaceTaskStatus } from "./types.js";
|
|
2
2
|
import type { AgentActivityItem, Mission, MissionAttempt, MissionReviewComment, MissionReviewStatus, MissionStatus } from "./mission-types.js";
|
|
3
3
|
import { type PasswordVault, type PasswordVaultItem, type PasswordVaultItemFilter, type PasswordVaultItemInput } from "./password-manager.js";
|
|
4
|
+
export interface ConnectorMeta {
|
|
5
|
+
provider: string;
|
|
6
|
+
apiUrl: string | null;
|
|
7
|
+
username: string | null;
|
|
8
|
+
connectedAt: string | null;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
}
|
|
4
11
|
type AuthPrincipalKind = "browser-admin" | "connected-app";
|
|
5
12
|
export type AuthScope = "admin" | "sessions" | "files" | "password-vault" | "session-preferences";
|
|
6
13
|
export interface AuthPrincipal {
|
|
@@ -91,6 +98,31 @@ export declare class WandStorage {
|
|
|
91
98
|
deleteWorkspaceTask(id: string, options?: {
|
|
92
99
|
cascade?: boolean;
|
|
93
100
|
}): void;
|
|
101
|
+
listWandTasks(workspaceId?: string | null): import("./task-types.js").WandTask[];
|
|
102
|
+
getWandTask(id: string): import("./task-types.js").WandTask | null;
|
|
103
|
+
createWandTask(input: {
|
|
104
|
+
workspaceId?: string | null;
|
|
105
|
+
title: string;
|
|
106
|
+
description?: string;
|
|
107
|
+
status?: import("./task-types.js").WandTaskStatus;
|
|
108
|
+
priority?: import("./task-types.js").WandTaskPriority;
|
|
109
|
+
labels?: string[];
|
|
110
|
+
dueDate?: string | null;
|
|
111
|
+
}): import("./task-types.js").WandTask;
|
|
112
|
+
updateWandTask(id: string, patch: Partial<Pick<import("./task-types.js").WandTask, "workspaceId" | "title" | "description" | "status" | "priority" | "labels" | "dueDate" | "sortOrder">>): import("./task-types.js").WandTask | null;
|
|
113
|
+
deleteWandTask(id: string): void;
|
|
114
|
+
listWandTaskSessionIds(taskId: string): string[];
|
|
115
|
+
bindWandTaskSession(taskId: string, sessionId: string): void;
|
|
116
|
+
unbindWandTaskSession(taskId: string, sessionId: string): void;
|
|
117
|
+
bindTaskboardTaskSession(taskId: string, sessionId: string): void;
|
|
118
|
+
listTaskboardTaskSessions(taskId: string): string[];
|
|
119
|
+
unbindTaskboardTaskSession(taskId: string, sessionId: string): void;
|
|
120
|
+
listGithubIssueBindings(owner: string, repo: string, issueNumber: number): Array<{
|
|
121
|
+
sessionId: string;
|
|
122
|
+
boundAt: string;
|
|
123
|
+
}>;
|
|
124
|
+
bindGithubIssueSession(owner: string, repo: string, issueNumber: number, sessionId: string): void;
|
|
125
|
+
unbindGithubIssueSession(owner: string, repo: string, issueNumber: number, sessionId: string): void;
|
|
94
126
|
listSessionsByWorkspaceTask(taskId: string): SessionSnapshot[];
|
|
95
127
|
setSessionWorkspaceTaskId(sessionId: string, taskId: string | null): void;
|
|
96
128
|
/** Get password from database */
|
|
@@ -115,6 +147,16 @@ export declare class WandStorage {
|
|
|
115
147
|
updatePasswordItem(id: string, input: PasswordVaultItemInput): PasswordVaultItem | null;
|
|
116
148
|
touchPasswordItem(id: string): PasswordVaultItem | null;
|
|
117
149
|
deletePasswordItem(id: string): boolean;
|
|
150
|
+
getConnectorToken(provider: string): string | null;
|
|
151
|
+
getConnectorMeta(provider: string): ConnectorMeta | null;
|
|
152
|
+
saveConnector(provider: string, input: {
|
|
153
|
+
token?: string | null;
|
|
154
|
+
apiUrl?: string | null;
|
|
155
|
+
username?: string | null;
|
|
156
|
+
markConnected?: boolean;
|
|
157
|
+
}): ConnectorMeta;
|
|
158
|
+
deleteConnector(provider: string): boolean;
|
|
159
|
+
private encryptConnectorToken;
|
|
118
160
|
saveAuthSession(token: string, expiresAt: number, principal?: AuthPrincipal): void;
|
|
119
161
|
getAuthSession(token: string): PersistedAuthSession | null;
|
|
120
162
|
deleteAuthSession(token: string): void;
|
package/dist/storage.js
CHANGED
|
@@ -545,6 +545,16 @@ const INIT_SQL = `
|
|
|
545
545
|
updated_at TEXT NOT NULL
|
|
546
546
|
);
|
|
547
547
|
|
|
548
|
+
CREATE TABLE IF NOT EXISTS connectors (
|
|
549
|
+
provider TEXT PRIMARY KEY,
|
|
550
|
+
token TEXT NOT NULL DEFAULT '',
|
|
551
|
+
api_url TEXT NOT NULL DEFAULT '',
|
|
552
|
+
username TEXT,
|
|
553
|
+
connected_at TEXT,
|
|
554
|
+
created_at TEXT NOT NULL,
|
|
555
|
+
updated_at TEXT NOT NULL
|
|
556
|
+
);
|
|
557
|
+
|
|
548
558
|
CREATE TABLE IF NOT EXISTS password_items (
|
|
549
559
|
id TEXT PRIMARY KEY,
|
|
550
560
|
vault_id TEXT NOT NULL,
|
|
@@ -663,7 +673,65 @@ const INIT_SQL = `
|
|
|
663
673
|
|
|
664
674
|
CREATE INDEX IF NOT EXISTS idx_workspace_tasks_workspace ON workspace_tasks(workspace_id);
|
|
665
675
|
CREATE INDEX IF NOT EXISTS idx_workspace_tasks_last_opened ON workspace_tasks(last_opened_at);
|
|
676
|
+
|
|
677
|
+
CREATE TABLE IF NOT EXISTS github_issue_bindings (
|
|
678
|
+
owner TEXT NOT NULL,
|
|
679
|
+
repo TEXT NOT NULL,
|
|
680
|
+
issue_number INTEGER NOT NULL,
|
|
681
|
+
session_id TEXT NOT NULL,
|
|
682
|
+
bound_at TEXT NOT NULL,
|
|
683
|
+
PRIMARY KEY (owner, repo, issue_number, session_id),
|
|
684
|
+
FOREIGN KEY (session_id) REFERENCES command_sessions(id) ON DELETE CASCADE
|
|
685
|
+
);
|
|
686
|
+
CREATE INDEX IF NOT EXISTS idx_github_issue_bindings_session ON github_issue_bindings(session_id);
|
|
687
|
+
|
|
688
|
+
CREATE TABLE IF NOT EXISTS wand_tasks (
|
|
689
|
+
id TEXT PRIMARY KEY,
|
|
690
|
+
identifier TEXT,
|
|
691
|
+
workspace_id TEXT,
|
|
692
|
+
title TEXT NOT NULL,
|
|
693
|
+
description TEXT NOT NULL DEFAULT '',
|
|
694
|
+
status TEXT NOT NULL DEFAULT 'todo',
|
|
695
|
+
priority TEXT NOT NULL DEFAULT 'none',
|
|
696
|
+
labels_json TEXT NOT NULL DEFAULT '[]',
|
|
697
|
+
due_date TEXT,
|
|
698
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
699
|
+
created_at TEXT NOT NULL,
|
|
700
|
+
updated_at TEXT NOT NULL,
|
|
701
|
+
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL
|
|
702
|
+
);
|
|
703
|
+
CREATE INDEX IF NOT EXISTS idx_wand_tasks_board ON wand_tasks(workspace_id, status, sort_order, updated_at);
|
|
704
|
+
CREATE TABLE IF NOT EXISTS wand_task_sessions (
|
|
705
|
+
task_id TEXT NOT NULL,
|
|
706
|
+
session_id TEXT NOT NULL,
|
|
707
|
+
bound_at TEXT NOT NULL,
|
|
708
|
+
PRIMARY KEY (task_id, session_id),
|
|
709
|
+
FOREIGN KEY (task_id) REFERENCES wand_tasks(id) ON DELETE CASCADE,
|
|
710
|
+
FOREIGN KEY (session_id) REFERENCES command_sessions(id) ON DELETE CASCADE
|
|
711
|
+
);
|
|
712
|
+
CREATE INDEX IF NOT EXISTS idx_wand_task_sessions_session ON wand_task_sessions(session_id);
|
|
713
|
+
CREATE TABLE IF NOT EXISTS taskboard_session_bindings (
|
|
714
|
+
task_id TEXT NOT NULL,
|
|
715
|
+
session_id TEXT NOT NULL,
|
|
716
|
+
bound_at TEXT NOT NULL,
|
|
717
|
+
PRIMARY KEY (task_id, session_id),
|
|
718
|
+
FOREIGN KEY (session_id) REFERENCES command_sessions(id) ON DELETE CASCADE
|
|
719
|
+
);
|
|
720
|
+
CREATE INDEX IF NOT EXISTS idx_taskboard_session_bindings_session ON taskboard_session_bindings(session_id);
|
|
666
721
|
`;
|
|
722
|
+
function ensureWandTaskSchema(db) {
|
|
723
|
+
const columns = db.prepare("PRAGMA table_info(wand_tasks)").all();
|
|
724
|
+
const names = new Set(columns.map((column) => column.name));
|
|
725
|
+
if (columns.length > 0 && !names.has("identifier"))
|
|
726
|
+
db.exec("ALTER TABLE wand_tasks ADD COLUMN identifier TEXT");
|
|
727
|
+
if (columns.length > 0 && !names.has("due_date"))
|
|
728
|
+
db.exec("ALTER TABLE wand_tasks ADD COLUMN due_date TEXT");
|
|
729
|
+
if (columns.length > 0) {
|
|
730
|
+
const rows = db.prepare("SELECT id FROM wand_tasks WHERE identifier IS NULL OR identifier = '' ORDER BY created_at, id").all();
|
|
731
|
+
const update = db.prepare("UPDATE wand_tasks SET identifier = ? WHERE id = ?");
|
|
732
|
+
rows.forEach((row, index) => update.run(`TASK-${index + 1}`, row.id));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
667
735
|
function ensureWorkspaceSchema(db) {
|
|
668
736
|
const workspaceColumns = db.prepare("PRAGMA table_info(workspaces)").all();
|
|
669
737
|
const workspaceNames = new Set(workspaceColumns.map((column) => column.name));
|
|
@@ -676,6 +744,23 @@ function ensureWorkspaceSchema(db) {
|
|
|
676
744
|
db.exec("ALTER TABLE workspace_tasks ADD COLUMN cwd TEXT");
|
|
677
745
|
}
|
|
678
746
|
}
|
|
747
|
+
function ensureConnectorSchema(db) {
|
|
748
|
+
// The table is created by INIT_SQL for new databases; this repairs an
|
|
749
|
+
// existing wand.db that predates the connector feature (additive-only).
|
|
750
|
+
const columns = db.prepare("PRAGMA table_info(connectors)").all();
|
|
751
|
+
const names = new Set(columns.map((column) => column.name));
|
|
752
|
+
if (names.size === 0)
|
|
753
|
+
return;
|
|
754
|
+
const migrations = [
|
|
755
|
+
["api_url", "ALTER TABLE connectors ADD COLUMN api_url TEXT NOT NULL DEFAULT ''"],
|
|
756
|
+
["username", "ALTER TABLE connectors ADD COLUMN username TEXT"],
|
|
757
|
+
["connected_at", "ALTER TABLE connectors ADD COLUMN connected_at TEXT"],
|
|
758
|
+
];
|
|
759
|
+
for (const [column, sql] of migrations) {
|
|
760
|
+
if (!names.has(column))
|
|
761
|
+
db.exec(sql);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
679
764
|
export function ensureDatabaseFile(dbPath) {
|
|
680
765
|
const dir = path.dirname(dbPath);
|
|
681
766
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -686,6 +771,7 @@ export function ensureDatabaseFile(dbPath) {
|
|
|
686
771
|
ensureAuthSessionSchema(db);
|
|
687
772
|
ensureCommandSessionSchema(db);
|
|
688
773
|
ensureWorkspaceSchema(db);
|
|
774
|
+
ensureConnectorSchema(db);
|
|
689
775
|
{
|
|
690
776
|
const missionColumns = db.prepare("PRAGMA table_info(missions)").all();
|
|
691
777
|
if (missionColumns.length > 0 && !missionColumns.some((column) => column.name === "task_id")) {
|
|
@@ -710,6 +796,8 @@ export class WandStorage {
|
|
|
710
796
|
ensureAuthSessionSchema(this.db);
|
|
711
797
|
ensureCommandSessionSchema(this.db);
|
|
712
798
|
ensureWorkspaceSchema(this.db);
|
|
799
|
+
ensureWandTaskSchema(this.db);
|
|
800
|
+
ensureConnectorSchema(this.db);
|
|
713
801
|
this.ensureDefaultPasswordVault();
|
|
714
802
|
}
|
|
715
803
|
directory() {
|
|
@@ -1008,6 +1096,86 @@ export class WandStorage {
|
|
|
1008
1096
|
}
|
|
1009
1097
|
this.db.prepare("DELETE FROM workspace_tasks WHERE id = ?").run(id);
|
|
1010
1098
|
}
|
|
1099
|
+
listWandTasks(workspaceId) {
|
|
1100
|
+
const rows = this.db.prepare(`SELECT id, identifier, workspace_id, title, description, status, priority, labels_json, due_date, sort_order, created_at, updated_at
|
|
1101
|
+
FROM wand_tasks ${workspaceId === undefined ? "" : "WHERE workspace_id IS ?"}
|
|
1102
|
+
ORDER BY status, sort_order, updated_at DESC`).all(...(workspaceId === undefined ? [] : [workspaceId]));
|
|
1103
|
+
return rows.map((row) => ({
|
|
1104
|
+
id: String(row.id),
|
|
1105
|
+
identifier: typeof row.identifier === "string" && row.identifier ? row.identifier : `TASK-${String(row.id).slice(0, 6)}`,
|
|
1106
|
+
workspaceId: typeof row.workspace_id === "string" ? row.workspace_id : null,
|
|
1107
|
+
title: String(row.title),
|
|
1108
|
+
description: String(row.description ?? ""),
|
|
1109
|
+
status: row.status === "doing" || row.status === "done" ? row.status : "todo",
|
|
1110
|
+
priority: row.priority === "low" || row.priority === "medium" || row.priority === "high" || row.priority === "urgent" ? row.priority : "none",
|
|
1111
|
+
labels: (() => { const parsed = safeJsonParse(String(row.labels_json ?? "[]")); return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : []; })(),
|
|
1112
|
+
dueDate: typeof row.due_date === "string" ? row.due_date : null,
|
|
1113
|
+
sortOrder: Number(row.sort_order) || 0,
|
|
1114
|
+
createdAt: String(row.created_at),
|
|
1115
|
+
updatedAt: String(row.updated_at),
|
|
1116
|
+
}));
|
|
1117
|
+
}
|
|
1118
|
+
getWandTask(id) {
|
|
1119
|
+
return this.listWandTasks().find((task) => task.id === id) ?? null;
|
|
1120
|
+
}
|
|
1121
|
+
createWandTask(input) {
|
|
1122
|
+
const id = crypto.randomUUID();
|
|
1123
|
+
const now = nowIso();
|
|
1124
|
+
const status = input.status ?? "todo";
|
|
1125
|
+
const priority = input.priority ?? "none";
|
|
1126
|
+
const max = this.db.prepare("SELECT COALESCE(MAX(sort_order), -1) AS value FROM wand_tasks WHERE workspace_id IS ? AND status = ?").get(input.workspaceId ?? null, status);
|
|
1127
|
+
const numberRow = this.db.prepare("SELECT COALESCE(MAX(CAST(substr(identifier, 6) AS INTEGER)), 0) AS value FROM wand_tasks WHERE identifier GLOB 'TASK-[0-9]*'").get();
|
|
1128
|
+
const identifier = `TASK-${(numberRow?.value ?? 0) + 1}`;
|
|
1129
|
+
this.db.prepare(`INSERT INTO wand_tasks (id, identifier, workspace_id, title, description, status, priority, labels_json, due_date, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, identifier, input.workspaceId ?? null, input.title, input.description ?? "", status, priority, JSON.stringify(input.labels ?? []), input.dueDate ?? null, (max?.value ?? -1) + 1, now, now);
|
|
1130
|
+
return this.getWandTask(id);
|
|
1131
|
+
}
|
|
1132
|
+
updateWandTask(id, patch) {
|
|
1133
|
+
const current = this.getWandTask(id);
|
|
1134
|
+
if (!current)
|
|
1135
|
+
return null;
|
|
1136
|
+
const next = { ...current, ...patch, updatedAt: nowIso() };
|
|
1137
|
+
this.db.prepare(`UPDATE wand_tasks SET workspace_id = ?, title = ?, description = ?, status = ?, priority = ?, labels_json = ?, due_date = ?, sort_order = ?, updated_at = ? WHERE id = ?`).run(next.workspaceId, next.title, next.description, next.status, next.priority, JSON.stringify(next.labels), next.dueDate, next.sortOrder, next.updatedAt, id);
|
|
1138
|
+
return next;
|
|
1139
|
+
}
|
|
1140
|
+
deleteWandTask(id) { this.db.prepare("DELETE FROM wand_tasks WHERE id = ?").run(id); }
|
|
1141
|
+
listWandTaskSessionIds(taskId) {
|
|
1142
|
+
const rows = this.db.prepare("SELECT session_id FROM wand_task_sessions WHERE task_id = ? ORDER BY bound_at ASC").all(taskId);
|
|
1143
|
+
return rows.map((row) => row.session_id);
|
|
1144
|
+
}
|
|
1145
|
+
bindWandTaskSession(taskId, sessionId) {
|
|
1146
|
+
if (!this.getWandTask(taskId))
|
|
1147
|
+
throw new Error("未找到该任务。");
|
|
1148
|
+
if (!this.getSession(sessionId))
|
|
1149
|
+
throw new Error("未找到该会话。");
|
|
1150
|
+
this.db.prepare("INSERT OR IGNORE INTO wand_task_sessions (task_id, session_id, bound_at) VALUES (?, ?, ?)").run(taskId, sessionId, nowIso());
|
|
1151
|
+
}
|
|
1152
|
+
unbindWandTaskSession(taskId, sessionId) { this.db.prepare("DELETE FROM wand_task_sessions WHERE task_id = ? AND session_id = ?").run(taskId, sessionId); }
|
|
1153
|
+
bindTaskboardTaskSession(taskId, sessionId) {
|
|
1154
|
+
if (!this.getSession(sessionId))
|
|
1155
|
+
throw new Error("未找到该会话。");
|
|
1156
|
+
this.db.prepare("INSERT OR IGNORE INTO taskboard_session_bindings (task_id, session_id, bound_at) VALUES (?, ?, ?)").run(taskId, sessionId, nowIso());
|
|
1157
|
+
}
|
|
1158
|
+
listTaskboardTaskSessions(taskId) {
|
|
1159
|
+
const rows = this.db.prepare("SELECT session_id FROM taskboard_session_bindings WHERE task_id = ? ORDER BY rowid ASC").all(taskId);
|
|
1160
|
+
return rows.map((row) => row.session_id);
|
|
1161
|
+
}
|
|
1162
|
+
unbindTaskboardTaskSession(taskId, sessionId) {
|
|
1163
|
+
this.db.prepare("DELETE FROM taskboard_session_bindings WHERE task_id = ? AND session_id = ?").run(taskId, sessionId);
|
|
1164
|
+
}
|
|
1165
|
+
listGithubIssueBindings(owner, repo, issueNumber) {
|
|
1166
|
+
const rows = this.db.prepare(`SELECT session_id, bound_at FROM github_issue_bindings
|
|
1167
|
+
WHERE owner = ? AND repo = ? AND issue_number = ? ORDER BY bound_at ASC`).all(owner, repo, issueNumber);
|
|
1168
|
+
return rows.map((row) => ({ sessionId: row.session_id, boundAt: row.bound_at }));
|
|
1169
|
+
}
|
|
1170
|
+
bindGithubIssueSession(owner, repo, issueNumber, sessionId) {
|
|
1171
|
+
if (!this.getSession(sessionId))
|
|
1172
|
+
throw new Error("未找到该会话。");
|
|
1173
|
+
this.db.prepare(`INSERT OR IGNORE INTO github_issue_bindings (owner, repo, issue_number, session_id, bound_at)
|
|
1174
|
+
VALUES (?, ?, ?, ?, ?)`).run(owner, repo, issueNumber, sessionId, nowIso());
|
|
1175
|
+
}
|
|
1176
|
+
unbindGithubIssueSession(owner, repo, issueNumber, sessionId) {
|
|
1177
|
+
this.db.prepare(`DELETE FROM github_issue_bindings WHERE owner = ? AND repo = ? AND issue_number = ? AND session_id = ?`).run(owner, repo, issueNumber, sessionId);
|
|
1178
|
+
}
|
|
1011
1179
|
listSessionsByWorkspaceTask(taskId) {
|
|
1012
1180
|
const rows = this.db
|
|
1013
1181
|
.prepare(`${sessionRowQuery("SELECT")}
|
|
@@ -1164,6 +1332,61 @@ export class WandStorage {
|
|
|
1164
1332
|
.run(now, id);
|
|
1165
1333
|
return result.changes > 0;
|
|
1166
1334
|
}
|
|
1335
|
+
// ============ Connector Methods ============
|
|
1336
|
+
// 第三方服务凭据(GitHub 等)。token 用与密码库同一把 at-rest 密钥加密,
|
|
1337
|
+
// 解密只在服务端发生,绝不通过 /api/settings 回传明文。
|
|
1338
|
+
getConnectorToken(provider) {
|
|
1339
|
+
const row = this.db
|
|
1340
|
+
.prepare("SELECT token FROM connectors WHERE provider = ?")
|
|
1341
|
+
.get(provider);
|
|
1342
|
+
if (!row)
|
|
1343
|
+
return null;
|
|
1344
|
+
const decrypted = decryptVaultSecret(row.token, this.getAppSecret());
|
|
1345
|
+
return decrypted ?? "";
|
|
1346
|
+
}
|
|
1347
|
+
getConnectorMeta(provider) {
|
|
1348
|
+
const row = this.db
|
|
1349
|
+
.prepare("SELECT provider, api_url, username, connected_at, updated_at FROM connectors WHERE provider = ?")
|
|
1350
|
+
.get(provider);
|
|
1351
|
+
if (!row)
|
|
1352
|
+
return null;
|
|
1353
|
+
return {
|
|
1354
|
+
provider: row.provider,
|
|
1355
|
+
apiUrl: row.api_url || null,
|
|
1356
|
+
username: row.username ?? null,
|
|
1357
|
+
connectedAt: row.connected_at ?? null,
|
|
1358
|
+
updatedAt: row.updated_at,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
saveConnector(provider, input) {
|
|
1362
|
+
const now = nowIso();
|
|
1363
|
+
const existing = this.getConnectorMeta(provider);
|
|
1364
|
+
const token = input.token === undefined || input.token === null || input.token === ""
|
|
1365
|
+
? this.getConnectorToken(provider) ?? ""
|
|
1366
|
+
: input.token;
|
|
1367
|
+
const apiUrl = input.apiUrl === undefined ? (existing?.apiUrl ?? "") : input.apiUrl;
|
|
1368
|
+
const username = input.username === undefined ? (existing?.username ?? null) : input.username;
|
|
1369
|
+
const connectedAt = input.markConnected === false ? (existing?.connectedAt ?? now) : now;
|
|
1370
|
+
const encrypted = this.encryptConnectorToken(token);
|
|
1371
|
+
this.db.prepare(`INSERT INTO connectors (provider, token, api_url, username, connected_at, created_at, updated_at)
|
|
1372
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1373
|
+
ON CONFLICT(provider) DO UPDATE SET
|
|
1374
|
+
token = excluded.token,
|
|
1375
|
+
api_url = excluded.api_url,
|
|
1376
|
+
username = excluded.username,
|
|
1377
|
+
connected_at = excluded.connected_at,
|
|
1378
|
+
updated_at = excluded.updated_at`).run(provider, encrypted, apiUrl, username, connectedAt, now, now);
|
|
1379
|
+
return { provider, apiUrl: apiUrl || null, username, connectedAt, updatedAt: now };
|
|
1380
|
+
}
|
|
1381
|
+
deleteConnector(provider) {
|
|
1382
|
+
return this.db.prepare("DELETE FROM connectors WHERE provider = ?").run(provider).changes > 0;
|
|
1383
|
+
}
|
|
1384
|
+
encryptConnectorToken(token) {
|
|
1385
|
+
if (!token)
|
|
1386
|
+
return "";
|
|
1387
|
+
const secret = this.getAppSecret();
|
|
1388
|
+
return secret ? encryptVaultSecret(token, secret) : token;
|
|
1389
|
+
}
|
|
1167
1390
|
// ============ Auth Session Methods ============
|
|
1168
1391
|
saveAuthSession(token, expiresAt, principal = { kind: "browser-admin", scopes: ["admin"] }) {
|
|
1169
1392
|
this.db
|
|
@@ -116,7 +116,8 @@ export function thinkingEffortToPiLevel(effort) {
|
|
|
116
116
|
return "low";
|
|
117
117
|
if (effort === "deep")
|
|
118
118
|
return "high";
|
|
119
|
+
// Pi uses `max`; `xhigh` is a Codex-only level and makes Pi reject the run.
|
|
119
120
|
if (effort === "max")
|
|
120
|
-
return "
|
|
121
|
+
return "max";
|
|
121
122
|
return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
|
|
122
123
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type WandTaskStatus = "todo" | "doing" | "done";
|
|
2
|
+
export type WandTaskPriority = "none" | "low" | "medium" | "high" | "urgent";
|
|
3
|
+
export interface WandTask {
|
|
4
|
+
id: string;
|
|
5
|
+
workspaceId: string | null;
|
|
6
|
+
identifier: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
status: WandTaskStatus;
|
|
10
|
+
priority: WandTaskPriority;
|
|
11
|
+
labels: string[];
|
|
12
|
+
dueDate: string | null;
|
|
13
|
+
sortOrder: number;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
}
|
|
17
|
+
export interface WandTaskDetail extends WandTask {
|
|
18
|
+
sessionIds: string[];
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RequestHandler } from "express";
|
|
2
|
+
import type { WandStorage } from "./storage.js";
|
|
3
|
+
import type { SessionRegistry } from "./session-registry.js";
|
|
4
|
+
import type { StructuredSessionManager } from "./structured-session-manager.js";
|
|
5
|
+
interface TaskboardBridge {
|
|
6
|
+
handler: RequestHandler;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function taskboardIntegrationScript(): string;
|
|
10
|
+
export declare function startTaskboardBridge(configDir: string, storage?: WandStorage, sessions?: SessionRegistry, structured?: StructuredSessionManager, fallbackCwd?: string): Promise<TaskboardBridge>;
|
|
11
|
+
export {};
|