@very_aq/codex-cli-web 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/README.zh-CN.md +228 -0
- package/package.json +38 -0
- package/server/dist/admin/userAdminRoutes.d.ts +7 -0
- package/server/dist/admin/userAdminRoutes.js +91 -0
- package/server/dist/admin/userAdminRoutes.js.map +1 -0
- package/server/dist/app.d.ts +22 -0
- package/server/dist/app.js +993 -0
- package/server/dist/app.js.map +1 -0
- package/server/dist/auth/adminInit.d.ts +41 -0
- package/server/dist/auth/adminInit.js +126 -0
- package/server/dist/auth/adminInit.js.map +1 -0
- package/server/dist/auth/bootstrapAdmin.d.ts +6 -0
- package/server/dist/auth/bootstrapAdmin.js +11 -0
- package/server/dist/auth/bootstrapAdmin.js.map +1 -0
- package/server/dist/auth/httpAuth.d.ts +16 -0
- package/server/dist/auth/httpAuth.js +31 -0
- package/server/dist/auth/httpAuth.js.map +1 -0
- package/server/dist/auth/password.d.ts +2 -0
- package/server/dist/auth/password.js +68 -0
- package/server/dist/auth/password.js.map +1 -0
- package/server/dist/auth/requireAdmin.d.ts +2 -0
- package/server/dist/auth/requireAdmin.js +14 -0
- package/server/dist/auth/requireAdmin.js.map +1 -0
- package/server/dist/auth/roles.d.ts +3 -0
- package/server/dist/auth/roles.js +13 -0
- package/server/dist/auth/roles.js.map +1 -0
- package/server/dist/auth/session.d.ts +24 -0
- package/server/dist/auth/session.js +127 -0
- package/server/dist/auth/session.js.map +1 -0
- package/server/dist/auth/sqlite/authDb.d.ts +18 -0
- package/server/dist/auth/sqlite/authDb.js +26 -0
- package/server/dist/auth/sqlite/authDb.js.map +1 -0
- package/server/dist/auth/sqlite/legacyImport.d.ts +22 -0
- package/server/dist/auth/sqlite/legacyImport.js +208 -0
- package/server/dist/auth/sqlite/legacyImport.js.map +1 -0
- package/server/dist/auth/sqlite/schema.d.ts +11 -0
- package/server/dist/auth/sqlite/schema.js +47 -0
- package/server/dist/auth/sqlite/schema.js.map +1 -0
- package/server/dist/auth/sqlite/sqliteUserStore.d.ts +12 -0
- package/server/dist/auth/sqlite/sqliteUserStore.js +194 -0
- package/server/dist/auth/sqlite/sqliteUserStore.js.map +1 -0
- package/server/dist/auth/userStore.d.ts +19 -0
- package/server/dist/auth/userStore.js +26 -0
- package/server/dist/auth/userStore.js.map +1 -0
- package/server/dist/auth/userTypes.d.ts +13 -0
- package/server/dist/auth/userTypes.js +3 -0
- package/server/dist/auth/userTypes.js.map +1 -0
- package/server/dist/chat/attachmentPathRedaction.d.ts +5 -0
- package/server/dist/chat/attachmentPathRedaction.js +67 -0
- package/server/dist/chat/attachmentPathRedaction.js.map +1 -0
- package/server/dist/chat/chatItemEnricher.d.ts +5 -0
- package/server/dist/chat/chatItemEnricher.js +40 -0
- package/server/dist/chat/chatItemEnricher.js.map +1 -0
- package/server/dist/chat/codexEventProjector.d.ts +33 -0
- package/server/dist/chat/codexEventProjector.js +482 -0
- package/server/dist/chat/codexEventProjector.js.map +1 -0
- package/server/dist/chat/contextUsageProjector.d.ts +10 -0
- package/server/dist/chat/contextUsageProjector.js +472 -0
- package/server/dist/chat/contextUsageProjector.js.map +1 -0
- package/server/dist/chat/fileChangeExtractor.d.ts +5 -0
- package/server/dist/chat/fileChangeExtractor.js +121 -0
- package/server/dist/chat/fileChangeExtractor.js.map +1 -0
- package/server/dist/chat/markdown/markdownAst.d.ts +5 -0
- package/server/dist/chat/markdown/markdownAst.js +154 -0
- package/server/dist/chat/markdown/markdownAst.js.map +1 -0
- package/server/dist/chat/systemToolCallSummary.d.ts +10 -0
- package/server/dist/chat/systemToolCallSummary.js +112 -0
- package/server/dist/chat/systemToolCallSummary.js.map +1 -0
- package/server/dist/chat/terminal/terminalPlainText.d.ts +14 -0
- package/server/dist/chat/terminal/terminalPlainText.js +139 -0
- package/server/dist/chat/terminal/terminalPlainText.js.map +1 -0
- package/server/dist/chat/textMetrics.d.ts +9 -0
- package/server/dist/chat/textMetrics.js +24 -0
- package/server/dist/chat/textMetrics.js.map +1 -0
- package/server/dist/chat/threadTurnsProjector.d.ts +12 -0
- package/server/dist/chat/threadTurnsProjector.js +292 -0
- package/server/dist/chat/threadTurnsProjector.js.map +1 -0
- package/server/dist/chat/todoPlanProjector.d.ts +8 -0
- package/server/dist/chat/todoPlanProjector.js +94 -0
- package/server/dist/chat/todoPlanProjector.js.map +1 -0
- package/server/dist/chat/todoPlanTypes.d.ts +21 -0
- package/server/dist/chat/todoPlanTypes.js +3 -0
- package/server/dist/chat/todoPlanTypes.js.map +1 -0
- package/server/dist/chat/types.d.ts +138 -0
- package/server/dist/chat/types.js +3 -0
- package/server/dist/chat/types.js.map +1 -0
- package/server/dist/cli/configArg.d.ts +21 -0
- package/server/dist/cli/configArg.js +70 -0
- package/server/dist/cli/configArg.js.map +1 -0
- package/server/dist/codex/appServerProcess.d.ts +24 -0
- package/server/dist/codex/appServerProcess.js +56 -0
- package/server/dist/codex/appServerProcess.js.map +1 -0
- package/server/dist/codex/cliArgs.d.ts +17 -0
- package/server/dist/codex/cliArgs.js +34 -0
- package/server/dist/codex/cliArgs.js.map +1 -0
- package/server/dist/codex/codexAppServer.d.ts +103 -0
- package/server/dist/codex/codexAppServer.js +206 -0
- package/server/dist/codex/codexAppServer.js.map +1 -0
- package/server/dist/codex/jsonl.d.ts +4 -0
- package/server/dist/codex/jsonl.js +23 -0
- package/server/dist/codex/jsonl.js.map +1 -0
- package/server/dist/codex/jsonrpc.d.ts +43 -0
- package/server/dist/codex/jsonrpc.js +96 -0
- package/server/dist/codex/jsonrpc.js.map +1 -0
- package/server/dist/config/serverConfig.d.ts +150 -0
- package/server/dist/config/serverConfig.js +64 -0
- package/server/dist/config/serverConfig.js.map +1 -0
- package/server/dist/env.d.ts +101 -0
- package/server/dist/env.js +523 -0
- package/server/dist/env.js.map +1 -0
- package/server/dist/history/http/historyRoutes.d.ts +18 -0
- package/server/dist/history/http/historyRoutes.js +67 -0
- package/server/dist/history/http/historyRoutes.js.map +1 -0
- package/server/dist/history/index.d.ts +24 -0
- package/server/dist/history/index.js +30 -0
- package/server/dist/history/index.js.map +1 -0
- package/server/dist/history/ingest/historyIngestService.d.ts +15 -0
- package/server/dist/history/ingest/historyIngestService.js +42 -0
- package/server/dist/history/ingest/historyIngestService.js.map +1 -0
- package/server/dist/history/projector/extractIds.d.ts +36 -0
- package/server/dist/history/projector/extractIds.js +111 -0
- package/server/dist/history/projector/extractIds.js.map +1 -0
- package/server/dist/history/projector/projectCodexEvent.d.ts +27 -0
- package/server/dist/history/projector/projectCodexEvent.js +845 -0
- package/server/dist/history/projector/projectCodexEvent.js.map +1 -0
- package/server/dist/history/query/historyQueryService.d.ts +34 -0
- package/server/dist/history/query/historyQueryService.js +170 -0
- package/server/dist/history/query/historyQueryService.js.map +1 -0
- package/server/dist/history/sqlite/schema.d.ts +11 -0
- package/server/dist/history/sqlite/schema.js +34 -0
- package/server/dist/history/sqlite/schema.js.map +1 -0
- package/server/dist/history/sqlite/sqliteHistoryStore.d.ts +69 -0
- package/server/dist/history/sqlite/sqliteHistoryStore.js +206 -0
- package/server/dist/history/sqlite/sqliteHistoryStore.js.map +1 -0
- package/server/dist/history/types.d.ts +29 -0
- package/server/dist/history/types.js +3 -0
- package/server/dist/history/types.js.map +1 -0
- package/server/dist/index.d.ts +1 -0
- package/server/dist/index.js +166 -0
- package/server/dist/index.js.map +1 -0
- package/server/dist/security/executionPolicy.d.ts +33 -0
- package/server/dist/security/executionPolicy.js +72 -0
- package/server/dist/security/executionPolicy.js.map +1 -0
- package/server/dist/security/origin.d.ts +11 -0
- package/server/dist/security/origin.js +40 -0
- package/server/dist/security/origin.js.map +1 -0
- package/server/dist/settings/sqliteUserSettingsStore.d.ts +12 -0
- package/server/dist/settings/sqliteUserSettingsStore.js +62 -0
- package/server/dist/settings/sqliteUserSettingsStore.js.map +1 -0
- package/server/dist/settings/userSettingsRoutes.d.ts +8 -0
- package/server/dist/settings/userSettingsRoutes.js +55 -0
- package/server/dist/settings/userSettingsRoutes.js.map +1 -0
- package/server/dist/settings/userSettingsStore.d.ts +19 -0
- package/server/dist/settings/userSettingsStore.js +26 -0
- package/server/dist/settings/userSettingsStore.js.map +1 -0
- package/server/dist/settings/userSettingsTypes.d.ts +70 -0
- package/server/dist/settings/userSettingsTypes.js +196 -0
- package/server/dist/settings/userSettingsTypes.js.map +1 -0
- package/server/dist/status/codexTaskTracker.d.ts +21 -0
- package/server/dist/status/codexTaskTracker.js +123 -0
- package/server/dist/status/codexTaskTracker.js.map +1 -0
- package/server/dist/status/threadContextUsage.d.ts +19 -0
- package/server/dist/status/threadContextUsage.js +229 -0
- package/server/dist/status/threadContextUsage.js.map +1 -0
- package/server/dist/threadList/threadListServerCache.d.ts +10 -0
- package/server/dist/threadList/threadListServerCache.js +42 -0
- package/server/dist/threadList/threadListServerCache.js.map +1 -0
- package/server/dist/tools/cwdSuggest.d.ts +11 -0
- package/server/dist/tools/cwdSuggest.js +128 -0
- package/server/dist/tools/cwdSuggest.js.map +1 -0
- package/server/dist/workspace/accessControl.d.ts +16 -0
- package/server/dist/workspace/accessControl.js +82 -0
- package/server/dist/workspace/accessControl.js.map +1 -0
- package/server/dist/workspace/sqliteUserWorkspaceStore.d.ts +12 -0
- package/server/dist/workspace/sqliteUserWorkspaceStore.js +82 -0
- package/server/dist/workspace/sqliteUserWorkspaceStore.js.map +1 -0
- package/server/dist/workspace/threadAccess.d.ts +19 -0
- package/server/dist/workspace/threadAccess.js +22 -0
- package/server/dist/workspace/threadAccess.js.map +1 -0
- package/server/dist/workspace/threadListVisibility.d.ts +25 -0
- package/server/dist/workspace/threadListVisibility.js +104 -0
- package/server/dist/workspace/threadListVisibility.js.map +1 -0
- package/server/dist/workspace/userWorkspaceRoutes.d.ts +7 -0
- package/server/dist/workspace/userWorkspaceRoutes.js +124 -0
- package/server/dist/workspace/userWorkspaceRoutes.js.map +1 -0
- package/server/dist/workspace/userWorkspaceStore.d.ts +12 -0
- package/server/dist/workspace/userWorkspaceStore.js +23 -0
- package/server/dist/workspace/userWorkspaceStore.js.map +1 -0
- package/server/dist/ws/socketIoBridge.d.ts +32 -0
- package/server/dist/ws/socketIoBridge.js +194 -0
- package/server/dist/ws/socketIoBridge.js.map +1 -0
- package/server/dist/ws/types.d.ts +113 -0
- package/server/dist/ws/types.js +3 -0
- package/server/dist/ws/types.js.map +1 -0
- package/server/dist/ws/wsHub.d.ts +119 -0
- package/server/dist/ws/wsHub.js +1259 -0
- package/server/dist/ws/wsHub.js.map +1 -0
- package/web/dist/assets/index-CY6cnwQz.js +174 -0
- package/web/dist/assets/index-DI7kJHr2.css +32 -0
- package/web/dist/favicon-mask.svg +9 -0
- package/web/dist/favicon.svg +26 -0
- package/web/dist/index.html +75 -0
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.createApp = createApp;
|
|
40
|
+
const express_1 = __importDefault(require("express"));
|
|
41
|
+
const busboy_1 = __importDefault(require("busboy"));
|
|
42
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
43
|
+
const fs_1 = __importDefault(require("fs"));
|
|
44
|
+
const path_1 = __importDefault(require("path"));
|
|
45
|
+
const env_1 = require("./env");
|
|
46
|
+
const httpAuth_1 = require("./auth/httpAuth");
|
|
47
|
+
const roles_1 = require("./auth/roles");
|
|
48
|
+
const bootstrapAdmin_1 = require("./auth/bootstrapAdmin");
|
|
49
|
+
const password_1 = require("./auth/password");
|
|
50
|
+
const adminInit_1 = require("./auth/adminInit");
|
|
51
|
+
const userAdminRoutes_1 = require("./admin/userAdminRoutes");
|
|
52
|
+
const cwdSuggest_1 = require("./tools/cwdSuggest");
|
|
53
|
+
const accessControl_1 = require("./workspace/accessControl");
|
|
54
|
+
const threadAccess_1 = require("./workspace/threadAccess");
|
|
55
|
+
const threadListVisibility_1 = require("./workspace/threadListVisibility");
|
|
56
|
+
const userWorkspaceRoutes_1 = require("./workspace/userWorkspaceRoutes");
|
|
57
|
+
const userSettingsRoutes_1 = require("./settings/userSettingsRoutes");
|
|
58
|
+
const userSettingsTypes_1 = require("./settings/userSettingsTypes");
|
|
59
|
+
const historyRoutes_1 = require("./history/http/historyRoutes");
|
|
60
|
+
const threadListServerCache_1 = require("./threadList/threadListServerCache");
|
|
61
|
+
const session_1 = require("./auth/session");
|
|
62
|
+
function createInMemoryUserStore() {
|
|
63
|
+
// 测试默认使用内存仓库,避免读写真实文件系统导致串扰。
|
|
64
|
+
const usersByUsername = new Map();
|
|
65
|
+
const normalizeUsername = (username) => String(username ?? "").trim();
|
|
66
|
+
const normalizeRole = (role) => (role === "admin" ? "admin" : "member");
|
|
67
|
+
return {
|
|
68
|
+
async createUser(input) {
|
|
69
|
+
const username = normalizeUsername(input.username);
|
|
70
|
+
if (!username)
|
|
71
|
+
throw new Error("username is required");
|
|
72
|
+
if (usersByUsername.has(username))
|
|
73
|
+
throw new Error("username already exists");
|
|
74
|
+
const password = String(input.password ?? "");
|
|
75
|
+
if (!password)
|
|
76
|
+
throw new Error("password is required");
|
|
77
|
+
// 这里复用 password.ts 的哈希逻辑,确保测试环境与生产一致。
|
|
78
|
+
const { hashPassword } = await Promise.resolve().then(() => __importStar(require("./auth/password")));
|
|
79
|
+
const passwordHash = await hashPassword(password);
|
|
80
|
+
const user = { username, role: normalizeRole(input.role), passwordHash, workspaces: [] };
|
|
81
|
+
usersByUsername.set(username, user);
|
|
82
|
+
return user;
|
|
83
|
+
},
|
|
84
|
+
async getUserByUsername(username) {
|
|
85
|
+
const normalized = normalizeUsername(username);
|
|
86
|
+
return usersByUsername.get(normalized) ?? null;
|
|
87
|
+
},
|
|
88
|
+
async setUserPassword(username, password) {
|
|
89
|
+
// 规范化用户名:作为 Map key 使用。
|
|
90
|
+
const normalizedUsername = normalizeUsername(username);
|
|
91
|
+
if (!normalizedUsername)
|
|
92
|
+
throw new Error("username is required");
|
|
93
|
+
// 新密码明文:仅用于计算 hash,不做持久化。
|
|
94
|
+
const nextPassword = String(password ?? "");
|
|
95
|
+
if (!nextPassword)
|
|
96
|
+
throw new Error("password is required");
|
|
97
|
+
// 定位目标用户;不存在则报错。
|
|
98
|
+
const user = usersByUsername.get(normalizedUsername);
|
|
99
|
+
if (!user)
|
|
100
|
+
throw new Error("user not found");
|
|
101
|
+
// 这里复用 password.ts 的哈希逻辑,确保测试环境与生产一致。
|
|
102
|
+
const { hashPassword } = await Promise.resolve().then(() => __importStar(require("./auth/password")));
|
|
103
|
+
// 计算新的 passwordHash 并回写。
|
|
104
|
+
const nextPasswordHash = await hashPassword(nextPassword);
|
|
105
|
+
user.passwordHash = nextPasswordHash;
|
|
106
|
+
usersByUsername.set(normalizedUsername, user);
|
|
107
|
+
return user;
|
|
108
|
+
},
|
|
109
|
+
async assignWorkspaces(username, workspaces) {
|
|
110
|
+
const normalized = normalizeUsername(username);
|
|
111
|
+
const user = usersByUsername.get(normalized);
|
|
112
|
+
if (!user)
|
|
113
|
+
throw new Error("user not found");
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
user.workspaces = (workspaces ?? []).map(String).map((w) => w.trim()).filter(Boolean).filter((w) => {
|
|
116
|
+
if (seen.has(w))
|
|
117
|
+
return false;
|
|
118
|
+
seen.add(w);
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
return user;
|
|
122
|
+
},
|
|
123
|
+
async listUsers() {
|
|
124
|
+
return [...usersByUsername.values()];
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function createInMemoryUserWorkspaceStore() {
|
|
129
|
+
// 用户工作目录内存仓库:测试默认值,避免依赖真实文件系统。
|
|
130
|
+
const workspaceDirsByUsername = new Map();
|
|
131
|
+
const normalizeUsername = (username) => String(username ?? "").trim();
|
|
132
|
+
return {
|
|
133
|
+
async listUserWorkspaceDirs(username) {
|
|
134
|
+
const normalizedUsername = normalizeUsername(username);
|
|
135
|
+
if (!normalizedUsername)
|
|
136
|
+
return [];
|
|
137
|
+
const workspaceDirs = workspaceDirsByUsername.get(normalizedUsername) ?? [];
|
|
138
|
+
return [...workspaceDirs];
|
|
139
|
+
},
|
|
140
|
+
async replaceUserWorkspaceDirs(username, workspaceDirs) {
|
|
141
|
+
const normalizedUsername = normalizeUsername(username);
|
|
142
|
+
if (!normalizedUsername)
|
|
143
|
+
throw new Error("username is required");
|
|
144
|
+
// 内存仓库仅做去重和绝对路径化,与文件仓库行为保持一致。
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
const normalizedWorkspaceDirs = (workspaceDirs ?? [])
|
|
147
|
+
.map((workspaceDir) => path_1.default.resolve(String(workspaceDir ?? "").trim()))
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.filter((workspaceDir) => {
|
|
150
|
+
if (seen.has(workspaceDir))
|
|
151
|
+
return false;
|
|
152
|
+
seen.add(workspaceDir);
|
|
153
|
+
return true;
|
|
154
|
+
});
|
|
155
|
+
workspaceDirsByUsername.set(normalizedUsername, normalizedWorkspaceDirs);
|
|
156
|
+
return [...normalizedWorkspaceDirs];
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function createInMemoryUserSettingsStore() {
|
|
161
|
+
// 用户配置内存仓库:测试默认值,避免依赖真实数据库。
|
|
162
|
+
const settingsByUsername = new Map();
|
|
163
|
+
const normalizeUsername = (username) => String(username ?? "").trim();
|
|
164
|
+
return {
|
|
165
|
+
async getUserSettings(username) {
|
|
166
|
+
const normalizedUsername = normalizeUsername(username);
|
|
167
|
+
if (!normalizedUsername)
|
|
168
|
+
return (0, userSettingsTypes_1.normalizeUserSettings)(null);
|
|
169
|
+
return (0, userSettingsTypes_1.normalizeUserSettings)(settingsByUsername.get(normalizedUsername) ?? null);
|
|
170
|
+
},
|
|
171
|
+
async replaceUserSettings(username, settings) {
|
|
172
|
+
const normalizedUsername = normalizeUsername(username);
|
|
173
|
+
if (!normalizedUsername)
|
|
174
|
+
throw new Error("username is required");
|
|
175
|
+
const normalizedSettings = (0, userSettingsTypes_1.normalizeUserSettings)(settings);
|
|
176
|
+
settingsByUsername.set(normalizedUsername, normalizedSettings);
|
|
177
|
+
return (0, userSettingsTypes_1.normalizeUserSettings)(normalizedSettings);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function createApp(opts) {
|
|
182
|
+
const app = (0, express_1.default)();
|
|
183
|
+
app.use(express_1.default.json({ limit: "1mb" }));
|
|
184
|
+
// 基础安全头:减少常见浏览器侧攻击面(不影响 API/前端功能)。
|
|
185
|
+
app.use((_req, res, next) => {
|
|
186
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
187
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
188
|
+
res.setHeader("Referrer-Policy", "same-origin");
|
|
189
|
+
next();
|
|
190
|
+
});
|
|
191
|
+
const userStore = opts.userStore ?? createInMemoryUserStore();
|
|
192
|
+
const userWorkspaceStore = opts.userWorkspaceStore ?? createInMemoryUserWorkspaceStore();
|
|
193
|
+
const userSettingsStore = opts.userSettingsStore ?? createInMemoryUserSettingsStore();
|
|
194
|
+
const adminBootstrap = (0, bootstrapAdmin_1.bootstrapAdmin)(userStore);
|
|
195
|
+
const threadListServerCache = (0, threadListServerCache_1.createThreadListServerCache)();
|
|
196
|
+
const listThreadsWithServerCache = async () => {
|
|
197
|
+
const cachedThreads = threadListServerCache.get();
|
|
198
|
+
if (cachedThreads)
|
|
199
|
+
return cachedThreads;
|
|
200
|
+
const latestThreads = await opts.codex.listThreads();
|
|
201
|
+
threadListServerCache.set(latestThreads);
|
|
202
|
+
return latestThreads;
|
|
203
|
+
};
|
|
204
|
+
const clearThreadListServerCache = () => {
|
|
205
|
+
threadListServerCache.clear();
|
|
206
|
+
};
|
|
207
|
+
// 登录限流:按 IP + username 做简单窗口限制,降低暴力破解风险(每 app 实例独立)。
|
|
208
|
+
const loginRateWindowMs = 30_000;
|
|
209
|
+
const loginRateMaxAttempts = 20;
|
|
210
|
+
const loginAttemptsByKey = new Map();
|
|
211
|
+
const getLoginRateKey = (req, username) => {
|
|
212
|
+
const xfwd = typeof req.headers["x-forwarded-for"] === "string" ? req.headers["x-forwarded-for"] : "";
|
|
213
|
+
const ip = xfwd.split(",")[0]?.trim() || String(req.socket?.remoteAddress ?? "unknown");
|
|
214
|
+
return `${ip}::${username}`;
|
|
215
|
+
};
|
|
216
|
+
const checkAndBumpLoginAttempt = (key) => {
|
|
217
|
+
const nowMs = Date.now();
|
|
218
|
+
// best-effort 清理过期 entry,避免长时间运行导致内存增长。
|
|
219
|
+
if (loginAttemptsByKey.size > 10_000) {
|
|
220
|
+
for (const [k, v] of loginAttemptsByKey.entries()) {
|
|
221
|
+
if (nowMs >= v.resetAtMs)
|
|
222
|
+
loginAttemptsByKey.delete(k);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const existing = loginAttemptsByKey.get(key);
|
|
226
|
+
if (!existing || nowMs >= existing.resetAtMs) {
|
|
227
|
+
loginAttemptsByKey.set(key, { count: 1, resetAtMs: nowMs + loginRateWindowMs });
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
existing.count += 1;
|
|
231
|
+
return existing.count > loginRateMaxAttempts;
|
|
232
|
+
};
|
|
233
|
+
const startedAtMs = opts.startedAtMs ?? Date.now();
|
|
234
|
+
const webDist = path_1.default.resolve(__dirname, "..", "..", "web", "dist");
|
|
235
|
+
app.get("/api/health", (_req, res) => res.json({ ok: true }));
|
|
236
|
+
app.get("/api/status", (_req, res) => {
|
|
237
|
+
const nowMs = Date.now();
|
|
238
|
+
const snapshot = opts.getStatusSnapshot?.() ?? {};
|
|
239
|
+
const webUiVersion = getWebUiVersion(webDist);
|
|
240
|
+
res.setHeader("Cache-Control", "no-store");
|
|
241
|
+
res.json({
|
|
242
|
+
ok: true,
|
|
243
|
+
nowMs,
|
|
244
|
+
server: { pid: process.pid, startedAtMs, uptimeMs: Math.max(0, nowMs - startedAtMs) },
|
|
245
|
+
webUi: { version: webUiVersion },
|
|
246
|
+
...snapshot,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
app.get("/api/auth/bootstrap-admin/status", (req, res) => {
|
|
250
|
+
void (async () => {
|
|
251
|
+
// 等待 bootstrap 完成,避免 setup/status 与 bootstrap 并发导致用户仓库写入冲突。
|
|
252
|
+
await adminBootstrap;
|
|
253
|
+
// 读取 admin 初始化状态:用于前端决定是否展示“首次设置密码”入口。
|
|
254
|
+
const state = await (0, adminInit_1.readAdminInitState)(userStore);
|
|
255
|
+
res.setHeader("Cache-Control", "no-store");
|
|
256
|
+
res.json({ ok: true, ...state });
|
|
257
|
+
})().catch((err) => {
|
|
258
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
app.post("/api/auth/bootstrap-admin/setup", (req, res) => {
|
|
262
|
+
void (async () => {
|
|
263
|
+
// 等待 bootstrap 完成,避免 setup/status 与 bootstrap 并发导致用户仓库写入冲突。
|
|
264
|
+
await adminBootstrap;
|
|
265
|
+
// setup 请求体:只关心新密码字段(前端可做二次确认校验)。
|
|
266
|
+
const body = (req.body ?? {});
|
|
267
|
+
const username = body.username;
|
|
268
|
+
const password = body.password;
|
|
269
|
+
const result = await (0, adminInit_1.setupAdminPassword)({ store: userStore, username, password });
|
|
270
|
+
if (!result.ok) {
|
|
271
|
+
if (result.status === "already_configured") {
|
|
272
|
+
res.status(409).json({ ok: false, error: "already_configured" });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (result.status === "invalid_username") {
|
|
276
|
+
res.status(400).json({ ok: false, error: "invalid_request", details: result.details });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
res.status(400).json({ ok: false, error: "invalid_request", details: result.details });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// 初始化成功后直接建立会话 cookie,避免用户再手动登录一次。
|
|
283
|
+
const secure = isSecureRequest(req);
|
|
284
|
+
const sessionUsername = String(result.user.username ?? "").trim() || "admin";
|
|
285
|
+
const cookieValue = (0, session_1.createSessionCookieValue)({ username: sessionUsername, role: "admin" }, opts.sessionSecret);
|
|
286
|
+
res.setHeader("Set-Cookie", (0, session_1.buildSessionSetCookieHeader)(cookieValue, { maxAgeSeconds: session_1.DEFAULT_SESSION_TTL_SECONDS, secure }));
|
|
287
|
+
res.json({ ok: true, status: result.status });
|
|
288
|
+
})().catch((err) => {
|
|
289
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
app.post("/api/auth/login", (req, res) => {
|
|
293
|
+
void (async () => {
|
|
294
|
+
await adminBootstrap;
|
|
295
|
+
const body = (req.body ?? {});
|
|
296
|
+
const username = String(body.username ?? "").trim();
|
|
297
|
+
const password = String(body.password ?? "");
|
|
298
|
+
if (!username || !password) {
|
|
299
|
+
res.status(401).json({ ok: false, error: "invalid_credentials" });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const rateLimitKey = getLoginRateKey(req, username);
|
|
303
|
+
const limited = checkAndBumpLoginAttempt(rateLimitKey);
|
|
304
|
+
if (limited) {
|
|
305
|
+
res.status(429).json({ ok: false, error: "rate_limited" });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const user = await userStore.getUserByUsername(username);
|
|
309
|
+
if (!user) {
|
|
310
|
+
res.status(401).json({ ok: false, error: "invalid_credentials" });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const ok = await (0, password_1.verifyPassword)(password, user.passwordHash);
|
|
314
|
+
if (!ok) {
|
|
315
|
+
res.status(401).json({ ok: false, error: "invalid_credentials" });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// 成功登录后清理该 key,避免正常用户被之前失败尝试影响。
|
|
319
|
+
loginAttemptsByKey.delete(rateLimitKey);
|
|
320
|
+
const secure = isSecureRequest(req);
|
|
321
|
+
const cookieValue = (0, session_1.createSessionCookieValue)({ username: user.username, role: user.role }, opts.sessionSecret);
|
|
322
|
+
res.setHeader("Set-Cookie", (0, session_1.buildSessionSetCookieHeader)(cookieValue, { maxAgeSeconds: session_1.DEFAULT_SESSION_TTL_SECONDS, secure }));
|
|
323
|
+
res.json({ ok: true });
|
|
324
|
+
})().catch((err) => {
|
|
325
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
app.post("/api/auth/logout", (req, res) => {
|
|
329
|
+
const secure = isSecureRequest(req);
|
|
330
|
+
res.setHeader("Set-Cookie", (0, session_1.buildSessionClearCookieHeader)({ secure }));
|
|
331
|
+
res.json({ ok: true });
|
|
332
|
+
});
|
|
333
|
+
app.get("/api/auth/me", (req, res) => {
|
|
334
|
+
void (async () => {
|
|
335
|
+
const cookies = (0, session_1.parseCookieHeader)(req.headers.cookie);
|
|
336
|
+
const raw = cookies[session_1.SESSION_COOKIE_NAME];
|
|
337
|
+
const session = raw ? (0, session_1.verifySessionCookieValueWithRole)(raw, opts.sessionSecret) : null;
|
|
338
|
+
if (!session) {
|
|
339
|
+
res.status(401).json({ ok: false, error: "unauthorized" });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const stored = await userStore.getUserByUsername(session.username);
|
|
343
|
+
const role = stored?.role ?? session.role ?? (session.username === "admin" ? "admin" : "member");
|
|
344
|
+
const storedWorkspaces = stored?.workspaces ?? [];
|
|
345
|
+
// 原始分配目录:仅用于前端提示“已分配但未生效”,不参与本接口鉴权判定。
|
|
346
|
+
const assignedWorkspaces = storedWorkspaces;
|
|
347
|
+
// 返回给前端的工作区列表必须是“当前用户实际可用”的工作区:
|
|
348
|
+
// - member:由管理员分配 workspaces;
|
|
349
|
+
// - admin:默认授权根目录为 `/`(右模糊/前缀匹配可覆盖所有路径);若用户数据中显式配置了 workspaces,则以其为准。
|
|
350
|
+
const access = (0, accessControl_1.resolveAllowedRootsForUser)({ role, workspaces: storedWorkspaces });
|
|
351
|
+
const workspaces = access.allowAnyRoot ? (storedWorkspaces.length ? storedWorkspaces : ["/"]) : access.roots;
|
|
352
|
+
// 兼容旧版前端:保留 `user` 字段为 string。
|
|
353
|
+
res.json({ ok: true, user: session.username, username: session.username, role, workspaces, assignedWorkspaces });
|
|
354
|
+
})().catch((err) => {
|
|
355
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
app.use("/api", (req, res, next) => {
|
|
359
|
+
if (req.path === "/health")
|
|
360
|
+
return next();
|
|
361
|
+
if (req.path === "/status")
|
|
362
|
+
return next();
|
|
363
|
+
if (req.path.startsWith("/auth/"))
|
|
364
|
+
return next();
|
|
365
|
+
return (0, httpAuth_1.requireAuth)(opts.sessionSecret, userStore)(req, res, next);
|
|
366
|
+
});
|
|
367
|
+
app.use("/api/admin", (0, userAdminRoutes_1.createUserAdminRoutes)({ userStore }));
|
|
368
|
+
app.use("/api/workspaces", (0, userWorkspaceRoutes_1.createUserWorkspaceRoutes)({ userWorkspaceStore }));
|
|
369
|
+
app.use("/api/user-settings", (0, userSettingsRoutes_1.createUserSettingsRoutes)({ userSettingsStore }));
|
|
370
|
+
const uploadHandler = async (req, res) => {
|
|
371
|
+
const username = String(req.user?.username ?? "").trim();
|
|
372
|
+
if (!username) {
|
|
373
|
+
res.status(401).json({ ok: false, error: "unauthorized" });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const result = await receiveAttachmentUpload(req, { username, maxBytes: (0, env_1.getWebMaxUploadBytes)() });
|
|
378
|
+
res.json({ ok: true, ...result });
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
const e = err;
|
|
382
|
+
const status = typeof e?.status === "number" && Number.isFinite(e.status) ? Math.floor(e.status) : 400;
|
|
383
|
+
res.status(status).json({ ok: false, error: "upload_failed", details: String(e?.message ?? err) });
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
app.post("/api/uploads/attachment", uploadHandler);
|
|
387
|
+
// Back-compat for older UIs.
|
|
388
|
+
app.post("/api/uploads/image", uploadHandler);
|
|
389
|
+
app.get("/api/uploads/:filename", async (req, res) => {
|
|
390
|
+
const username = String(req.user?.username ?? "").trim();
|
|
391
|
+
if (!username) {
|
|
392
|
+
res.status(401).json({ ok: false, error: "unauthorized" });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const raw = String(req.params.filename ?? "");
|
|
396
|
+
const filename = sanitizeUploadedFilename(raw);
|
|
397
|
+
if (!filename) {
|
|
398
|
+
res.status(404).end();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const { dir } = getUserUploadsDir(username);
|
|
402
|
+
const fullPath = path_1.default.join(dir, filename);
|
|
403
|
+
try {
|
|
404
|
+
const st = await fs_1.default.promises.stat(fullPath);
|
|
405
|
+
if (!st.isFile()) {
|
|
406
|
+
res.status(404).end();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
res.status(404).end();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
res.setHeader("Cache-Control", "no-store");
|
|
415
|
+
res.setHeader("Content-Type", "application/octet-stream");
|
|
416
|
+
res.setHeader("Content-Disposition", `attachment; filename="${filename.replace(/\"/g, "")}"`);
|
|
417
|
+
res.sendFile(fullPath);
|
|
418
|
+
});
|
|
419
|
+
app.get("/api/config", async (_req, res) => {
|
|
420
|
+
const approvalPolicy = (0, env_1.getCodexApprovalPolicy)();
|
|
421
|
+
const sandboxMode = (0, env_1.getCodexSandboxMode)();
|
|
422
|
+
let model = null;
|
|
423
|
+
let reasoningEffort = null;
|
|
424
|
+
if (opts.codex) {
|
|
425
|
+
try {
|
|
426
|
+
const [collabRes, modelRes] = await Promise.allSettled([opts.codex.listCollaborationModes(), opts.codex.listModels()]);
|
|
427
|
+
const collabData = collabRes.status === "fulfilled" ? collabRes.value : null;
|
|
428
|
+
const collabItems = Array.isArray(collabData?.data) ? collabData.data : [];
|
|
429
|
+
const lowerIncludes = (s, needle) => String(s ?? "").toLowerCase().includes(needle);
|
|
430
|
+
const defaultCollab = collabItems.find((m) => m && typeof m === "object" && m.mode === "default") ??
|
|
431
|
+
collabItems.find((m) => m && typeof m === "object" && lowerIncludes(m.name, "default")) ??
|
|
432
|
+
null;
|
|
433
|
+
const collabModel = String(defaultCollab?.model ?? "").trim();
|
|
434
|
+
const collabEffortRaw = defaultCollab?.reasoning_effort;
|
|
435
|
+
const collabEffort = typeof collabEffortRaw === "string" ? collabEffortRaw.trim() : collabEffortRaw ?? null;
|
|
436
|
+
if (collabModel)
|
|
437
|
+
model = collabModel;
|
|
438
|
+
if (typeof collabEffort === "string" && collabEffort.trim())
|
|
439
|
+
reasoningEffort = collabEffort.trim();
|
|
440
|
+
const modelData = modelRes.status === "fulfilled" ? modelRes.value : null;
|
|
441
|
+
const modelItems = Array.isArray(modelData?.data) ? modelData.data : [];
|
|
442
|
+
const defaultModel = modelItems.find((m) => m && typeof m === "object" && Boolean(m.isDefault)) ??
|
|
443
|
+
modelItems.find((m) => m && typeof m === "object" && String(m.id ?? "").toLowerCase().includes("default")) ??
|
|
444
|
+
null;
|
|
445
|
+
if (!model) {
|
|
446
|
+
const fallbackModel = String(defaultModel?.model ?? defaultModel?.id ?? "").trim();
|
|
447
|
+
if (fallbackModel)
|
|
448
|
+
model = fallbackModel;
|
|
449
|
+
}
|
|
450
|
+
if (!reasoningEffort) {
|
|
451
|
+
const d = String(defaultModel?.defaultReasoningEffort ?? "").trim();
|
|
452
|
+
if (d)
|
|
453
|
+
reasoningEffort = d;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
// Best-effort only: still return env-derived defaults below.
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Security: avoid leaking server directory structure (e.g. CODEX_CWD) to clients.
|
|
461
|
+
res.json({ approvalPolicy, sandboxMode, model, reasoningEffort });
|
|
462
|
+
});
|
|
463
|
+
app.get("/api/cwd/suggest", async (req, res) => {
|
|
464
|
+
const q = typeof req.query?.q === "string" ? String(req.query.q) : "";
|
|
465
|
+
const limit = typeof req.query?.limit === "string" ? Number(req.query.limit) : undefined;
|
|
466
|
+
const access = (0, accessControl_1.resolveAllowedRootsForUser)(req.user);
|
|
467
|
+
if (!access.allowAnyRoot && !access.roots.length) {
|
|
468
|
+
res.status(403).json({ ok: false, error: "path_not_allowed" });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const suggestions = await (0, cwdSuggest_1.suggestCwds)({
|
|
473
|
+
query: q,
|
|
474
|
+
limit,
|
|
475
|
+
allowAnyRoot: access.allowAnyRoot,
|
|
476
|
+
allowedRoots: access.allowAnyRoot ? undefined : access.roots,
|
|
477
|
+
});
|
|
478
|
+
res.json({ ok: true, suggestions });
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
if (err instanceof cwdSuggest_1.CwdSuggestError) {
|
|
482
|
+
const status = err.code === "path_not_allowed" ? 403 : 400;
|
|
483
|
+
res.status(status).json({ ok: false, error: err.code });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
res.status(500).json({ ok: false, error: "request_failed" });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
app.get("/api/model/list", async (_req, res) => {
|
|
490
|
+
if (!opts.codex) {
|
|
491
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const result = await opts.codex.listModels();
|
|
496
|
+
res.json({ ok: true, ...result });
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
app.get("/api/experimental/list", async (_req, res) => {
|
|
503
|
+
if (!opts.codex) {
|
|
504
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const result = await opts.codex.listExperimentalFeatures();
|
|
509
|
+
res.json({ ok: true, ...result });
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
app.get("/api/collaboration/list", async (_req, res) => {
|
|
516
|
+
if (!opts.codex) {
|
|
517
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const result = await opts.codex.listCollaborationModes();
|
|
522
|
+
res.json({ ok: true, ...result });
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
app.post("/api/experimental/set", async (req, res) => {
|
|
529
|
+
if (!opts.codex) {
|
|
530
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!(0, roles_1.isAdminRole)(req.user?.role)) {
|
|
534
|
+
res.status(403).json({ ok: false, error: "forbidden" });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const body = (req.body ?? {});
|
|
538
|
+
const name = String(body.name ?? "").trim();
|
|
539
|
+
const enabled = Boolean(body.enabled);
|
|
540
|
+
if (!name) {
|
|
541
|
+
res.status(400).json({ ok: false, error: "invalid_request" });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const result = await opts.codex.setConfigValue({ keyPath: `features.${name}`, value: enabled, mergeStrategy: "replace" });
|
|
546
|
+
res.json({ ok: true, ...result });
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
app.get("/api/skills/list", async (_req, res) => {
|
|
553
|
+
if (!opts.codex) {
|
|
554
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const result = await opts.codex.listSkills();
|
|
559
|
+
res.json({ ok: true, ...result });
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
app.post("/api/skills/set", async (req, res) => {
|
|
566
|
+
if (!opts.codex) {
|
|
567
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (!(0, roles_1.isAdminRole)(req.user?.role)) {
|
|
571
|
+
res.status(403).json({ ok: false, error: "forbidden" });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const body = (req.body ?? {});
|
|
575
|
+
const skillPath = String(body.path ?? "").trim();
|
|
576
|
+
const enabled = Boolean(body.enabled);
|
|
577
|
+
if (!skillPath) {
|
|
578
|
+
res.status(400).json({ ok: false, error: "invalid_request" });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
const result = await opts.codex.setSkillEnabled({ path: skillPath, enabled });
|
|
583
|
+
res.json({ ok: true, ...result });
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
app.get("/api/thread/list", async (req, res) => {
|
|
590
|
+
if (!opts.codex) {
|
|
591
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const user = req.user;
|
|
595
|
+
if (!user) {
|
|
596
|
+
res.status(401).json({ ok: false, error: "unauthorized" });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const requestedCwd = typeof req.query?.cwd === "string" ? String(req.query.cwd) : "";
|
|
600
|
+
let workspaceFilterCwd = null;
|
|
601
|
+
try {
|
|
602
|
+
workspaceFilterCwd = await (0, threadListVisibility_1.resolveThreadListWorkspaceFilter)(requestedCwd, user);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
// 统一返回 path_not_allowed,避免暴露目录是否存在等细节。
|
|
606
|
+
res.status(403).json({ ok: false, error: "path_not_allowed" });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
const routingSnapshot = await userStore
|
|
611
|
+
.listUsers()
|
|
612
|
+
.then((users) => (0, threadListVisibility_1.buildWorkspaceRoutingSnapshot)(users))
|
|
613
|
+
.catch(() => null);
|
|
614
|
+
const threads = await listThreadsWithServerCache();
|
|
615
|
+
const visibleThreads = threads.filter((thread) => (0, threadListVisibility_1.shouldIncludeThreadForList)({
|
|
616
|
+
thread,
|
|
617
|
+
user,
|
|
618
|
+
workspaceFilterCwd,
|
|
619
|
+
routingSnapshot,
|
|
620
|
+
}));
|
|
621
|
+
res.setHeader("Cache-Control", "no-store");
|
|
622
|
+
res.json({ ok: true, threads: visibleThreads, cwd: workspaceFilterCwd });
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
// 线程写接口统一鉴权:member 必须在其分配 workspaces 内操作 thread.cwd。
|
|
629
|
+
const requireThreadAccess = async (req, res, threadId) => {
|
|
630
|
+
const user = req.user;
|
|
631
|
+
if (!user) {
|
|
632
|
+
res.status(401).json({ ok: false, error: "unauthorized" });
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
await (0, threadAccess_1.assertThreadAllowedForUser)({ threadId, user, codex: opts.codex });
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
// 不区分 thread 不存在 / 越权等细节,避免信息泄露。
|
|
641
|
+
res.status(403).json({ ok: false, error: "forbidden" });
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
// 历史读取接口:复用线程访问权限校验,避免绕过现有工作区隔离规则。
|
|
646
|
+
if (opts.historyQuery) {
|
|
647
|
+
app.use("/api/history", (0, historyRoutes_1.createHistoryRoutes)({
|
|
648
|
+
historyQuery: opts.historyQuery,
|
|
649
|
+
requireThreadAccess,
|
|
650
|
+
}));
|
|
651
|
+
}
|
|
652
|
+
app.post("/api/thread/name/set", async (req, res) => {
|
|
653
|
+
if (!opts.codex) {
|
|
654
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const body = (req.body ?? {});
|
|
658
|
+
const threadId = String(body.threadId ?? "").trim();
|
|
659
|
+
const name = String(body.name ?? "").trim();
|
|
660
|
+
if (!threadId || !name) {
|
|
661
|
+
res.status(400).json({ ok: false, error: "invalid_request" });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
if (!(await requireThreadAccess(req, res, threadId)))
|
|
666
|
+
return;
|
|
667
|
+
const result = await opts.codex.setThreadName({ threadId, name });
|
|
668
|
+
clearThreadListServerCache();
|
|
669
|
+
res.json({ ok: true, ...result });
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
app.post("/api/review/start", async (req, res) => {
|
|
676
|
+
if (!opts.codex) {
|
|
677
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const body = (req.body ?? {});
|
|
681
|
+
const threadId = String(body.threadId ?? "").trim();
|
|
682
|
+
const target = body.target ?? { type: "uncommittedChanges" };
|
|
683
|
+
if (!threadId) {
|
|
684
|
+
res.status(400).json({ ok: false, error: "invalid_request" });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
if (!(await requireThreadAccess(req, res, threadId)))
|
|
689
|
+
return;
|
|
690
|
+
const result = await opts.codex.startReview({ threadId, target });
|
|
691
|
+
clearThreadListServerCache();
|
|
692
|
+
res.json({ ok: true, ...result });
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
app.post("/api/thread/compact/start", async (req, res) => {
|
|
699
|
+
if (!opts.codex) {
|
|
700
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const body = (req.body ?? {});
|
|
704
|
+
const threadId = String(body.threadId ?? "").trim();
|
|
705
|
+
if (!threadId) {
|
|
706
|
+
res.status(400).json({ ok: false, error: "invalid_request" });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
if (!(await requireThreadAccess(req, res, threadId)))
|
|
711
|
+
return;
|
|
712
|
+
const result = await opts.codex.startCompact({ threadId });
|
|
713
|
+
clearThreadListServerCache();
|
|
714
|
+
res.json({ ok: true, ...result });
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
app.post("/api/thread/fork", async (req, res) => {
|
|
721
|
+
if (!opts.codex) {
|
|
722
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const body = (req.body ?? {});
|
|
726
|
+
const threadId = String(body.threadId ?? "").trim();
|
|
727
|
+
if (!threadId) {
|
|
728
|
+
res.status(400).json({ ok: false, error: "invalid_request" });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
if (!(await requireThreadAccess(req, res, threadId)))
|
|
733
|
+
return;
|
|
734
|
+
const result = await opts.codex.forkThread({ threadId });
|
|
735
|
+
const thread = result?.thread ?? result;
|
|
736
|
+
const forkedId = String(thread?.id ?? "");
|
|
737
|
+
clearThreadListServerCache();
|
|
738
|
+
res.json({ ok: true, threadId: forkedId || undefined, thread });
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
app.post("/api/thread/archive", async (req, res) => {
|
|
745
|
+
if (!opts.codex) {
|
|
746
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const body = (req.body ?? {});
|
|
750
|
+
const threadId = String(body.threadId ?? "").trim();
|
|
751
|
+
if (!threadId) {
|
|
752
|
+
res.status(400).json({ ok: false, error: "invalid_request" });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
if (!(await requireThreadAccess(req, res, threadId)))
|
|
757
|
+
return;
|
|
758
|
+
const result = await opts.codex.archiveThread({ threadId });
|
|
759
|
+
clearThreadListServerCache();
|
|
760
|
+
res.json({ ok: true, ...result });
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
app.post("/api/thread/unarchive", async (req, res) => {
|
|
767
|
+
if (!opts.codex) {
|
|
768
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const body = (req.body ?? {});
|
|
772
|
+
const threadId = String(body.threadId ?? "").trim();
|
|
773
|
+
if (!threadId) {
|
|
774
|
+
res.status(400).json({ ok: false, error: "invalid_request" });
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
if (!(await requireThreadAccess(req, res, threadId)))
|
|
779
|
+
return;
|
|
780
|
+
const result = await opts.codex.unarchiveThread({ threadId });
|
|
781
|
+
clearThreadListServerCache();
|
|
782
|
+
res.json({ ok: true, ...result });
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
app.post("/api/thread/rollback", async (req, res) => {
|
|
789
|
+
if (!opts.codex) {
|
|
790
|
+
res.status(501).json({ ok: false, error: "not_supported" });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const body = (req.body ?? {});
|
|
794
|
+
const threadId = String(body.threadId ?? "").trim();
|
|
795
|
+
const numTurns = Number(body.numTurns ?? 0);
|
|
796
|
+
if (!threadId || !Number.isFinite(numTurns) || numTurns < 1) {
|
|
797
|
+
res.status(400).json({ ok: false, error: "invalid_request" });
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
if (!(await requireThreadAccess(req, res, threadId)))
|
|
802
|
+
return;
|
|
803
|
+
const result = await opts.codex.rollbackThread({ threadId, numTurns: Math.floor(numTurns) });
|
|
804
|
+
clearThreadListServerCache();
|
|
805
|
+
res.json({ ok: true, ...result });
|
|
806
|
+
}
|
|
807
|
+
catch (err) {
|
|
808
|
+
res.status(500).json({ ok: false, error: "request_failed", details: String(err) });
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
// API 路由兜底:未命中的 `/api/*` 统一返回 JSON 404,避免落到 HTML SPA fallback。
|
|
812
|
+
app.use("/api", (_req, res) => {
|
|
813
|
+
res.status(404).json({ ok: false, error: "not_found" });
|
|
814
|
+
});
|
|
815
|
+
if (fs_1.default.existsSync(webDist)) {
|
|
816
|
+
app.use(express_1.default.static(webDist, { index: false }));
|
|
817
|
+
app.get("*", (_req, res) => {
|
|
818
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
819
|
+
res.sendFile(path_1.default.join(webDist, "index.html"));
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return app;
|
|
823
|
+
}
|
|
824
|
+
function isSecureRequest(req) {
|
|
825
|
+
if (req.secure)
|
|
826
|
+
return true;
|
|
827
|
+
const xfProto = String(req.headers["x-forwarded-proto"] ?? "");
|
|
828
|
+
return xfProto.toLowerCase() === "https";
|
|
829
|
+
}
|
|
830
|
+
function sanitizeUploadedFilename(input) {
|
|
831
|
+
const name = input.trim();
|
|
832
|
+
if (!name)
|
|
833
|
+
return null;
|
|
834
|
+
if (name.includes("/") || name.includes("\\") || name.includes("\0"))
|
|
835
|
+
return null;
|
|
836
|
+
// Only allow our generated names.
|
|
837
|
+
if (!/^(?:img|att)-\d{13}-[a-f0-9]{16}(?:\.[a-z0-9]{1,10})?$/i.test(name))
|
|
838
|
+
return null;
|
|
839
|
+
return name;
|
|
840
|
+
}
|
|
841
|
+
function isPathWithinRoot(root, candidate) {
|
|
842
|
+
const rel = path_1.default.relative(root, candidate);
|
|
843
|
+
if (!rel)
|
|
844
|
+
return true;
|
|
845
|
+
if (rel.startsWith(".."))
|
|
846
|
+
return false;
|
|
847
|
+
if (path_1.default.isAbsolute(rel))
|
|
848
|
+
return false;
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
851
|
+
function getUserUploadsDir(username) {
|
|
852
|
+
// 用户名只保留安全字符,避免目录穿越和非法文件名。
|
|
853
|
+
// Keep only safe chars in username to avoid traversal/invalid paths.
|
|
854
|
+
const safeUser = username.replace(/[^a-zA-Z0-9._-]/g, "_") || "user";
|
|
855
|
+
// 计算 Codex 工作目录和上传根目录,用于回传“是否在 Codex CWD 内”标记。
|
|
856
|
+
// Resolve Codex cwd and upload base dir to compute `withinCodexCwd`.
|
|
857
|
+
const codexCwd = path_1.default.resolve((0, env_1.getCodexCwd)());
|
|
858
|
+
const uploadBaseDir = path_1.default.resolve((0, env_1.getWebUploadBaseDir)());
|
|
859
|
+
const withinCodexCwd = isPathWithinRoot(codexCwd, uploadBaseDir);
|
|
860
|
+
// 每个用户隔离子目录,避免命名冲突。
|
|
861
|
+
// Per-user subdirectory isolation to avoid collisions.
|
|
862
|
+
const dir = path_1.default.join(uploadBaseDir, safeUser);
|
|
863
|
+
return { dir, withinCodexCwd };
|
|
864
|
+
}
|
|
865
|
+
function safeExtFromFilename(filename) {
|
|
866
|
+
const ext = path_1.default.extname(filename || "").toLowerCase();
|
|
867
|
+
if (!ext)
|
|
868
|
+
return "";
|
|
869
|
+
if (ext.length > 12)
|
|
870
|
+
return "";
|
|
871
|
+
if (!/^\.[a-z0-9]+$/.test(ext))
|
|
872
|
+
return "";
|
|
873
|
+
return ext;
|
|
874
|
+
}
|
|
875
|
+
function getWebUiVersion(webDist) {
|
|
876
|
+
const indexPath = path_1.default.join(webDist, "index.html");
|
|
877
|
+
try {
|
|
878
|
+
const stat = fs_1.default.statSync(indexPath);
|
|
879
|
+
if (!stat.isFile())
|
|
880
|
+
return null;
|
|
881
|
+
return `${Math.floor(stat.mtimeMs)}-${stat.size}`;
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async function receiveAttachmentUpload(req, opts) {
|
|
888
|
+
const contentType = String(req.headers["content-type"] ?? "");
|
|
889
|
+
if (!contentType.toLowerCase().startsWith("multipart/form-data")) {
|
|
890
|
+
throw Object.assign(new Error("expected multipart/form-data"), { status: 415 });
|
|
891
|
+
}
|
|
892
|
+
const { dir, withinCodexCwd } = getUserUploadsDir(opts.username);
|
|
893
|
+
await fs_1.default.promises.mkdir(dir, { recursive: true });
|
|
894
|
+
const bb = (0, busboy_1.default)({
|
|
895
|
+
headers: req.headers,
|
|
896
|
+
limits: { files: 1, fileSize: Math.max(1, Math.floor(opts.maxBytes)) },
|
|
897
|
+
});
|
|
898
|
+
let done = false;
|
|
899
|
+
let fileFound = false;
|
|
900
|
+
let uploadError = null;
|
|
901
|
+
let localPathToCleanup = null;
|
|
902
|
+
let saved = null;
|
|
903
|
+
let writeStream = null;
|
|
904
|
+
let writePromise = null;
|
|
905
|
+
let writeReject = null;
|
|
906
|
+
const finishWrite = async () => {
|
|
907
|
+
if (!writePromise)
|
|
908
|
+
return;
|
|
909
|
+
await writePromise;
|
|
910
|
+
};
|
|
911
|
+
const abortWrite = async (err) => {
|
|
912
|
+
if (done)
|
|
913
|
+
return;
|
|
914
|
+
done = true;
|
|
915
|
+
uploadError = err;
|
|
916
|
+
if (writeReject)
|
|
917
|
+
writeReject(err);
|
|
918
|
+
try {
|
|
919
|
+
writeStream?.destroy();
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
// ignore
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
bb.on("file", (_fieldname, file, info) => {
|
|
926
|
+
fileFound = true;
|
|
927
|
+
const originalName = String(info?.filename ?? "attachment").trim() || "attachment";
|
|
928
|
+
const mime = String(info?.mimeType ?? info?.mimetype ?? "application/octet-stream").trim().toLowerCase();
|
|
929
|
+
const ext = safeExtFromFilename(originalName);
|
|
930
|
+
const filename = `att-${Date.now()}-${crypto_1.default.randomBytes(8).toString("hex")}${ext}`;
|
|
931
|
+
const localPath = path_1.default.join(dir, filename);
|
|
932
|
+
const apiUrl = `/api/uploads/${encodeURIComponent(filename)}`;
|
|
933
|
+
localPathToCleanup = localPath;
|
|
934
|
+
let size = 0;
|
|
935
|
+
writeStream = fs_1.default.createWriteStream(localPath, { flags: "wx" });
|
|
936
|
+
writePromise = new Promise((resolve, reject) => {
|
|
937
|
+
writeReject = reject;
|
|
938
|
+
writeStream.on("error", reject);
|
|
939
|
+
writeStream.on("finish", resolve);
|
|
940
|
+
});
|
|
941
|
+
// Avoid Node's PromiseRejectionHandledWarning when we intentionally reject early (e.g. size limit).
|
|
942
|
+
writePromise.catch(() => { });
|
|
943
|
+
file.on("data", (chunk) => {
|
|
944
|
+
size += Buffer.byteLength(chunk);
|
|
945
|
+
});
|
|
946
|
+
file.on("limit", () => {
|
|
947
|
+
file.unpipe(writeStream);
|
|
948
|
+
file.resume();
|
|
949
|
+
void abortWrite(Object.assign(new Error("file too large"), { status: 413 }));
|
|
950
|
+
});
|
|
951
|
+
file.on("error", (err) => {
|
|
952
|
+
void abortWrite(err);
|
|
953
|
+
});
|
|
954
|
+
file.on("end", () => {
|
|
955
|
+
if (file.truncated) {
|
|
956
|
+
void abortWrite(Object.assign(new Error("file too large"), { status: 413 }));
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
saved = { filename, originalName, mime, size, localPath, apiUrl };
|
|
960
|
+
});
|
|
961
|
+
file.pipe(writeStream);
|
|
962
|
+
});
|
|
963
|
+
try {
|
|
964
|
+
const result = await new Promise((resolve, reject) => {
|
|
965
|
+
bb.on("error", reject);
|
|
966
|
+
bb.on("finish", () => resolve(saved));
|
|
967
|
+
req.pipe(bb);
|
|
968
|
+
});
|
|
969
|
+
await finishWrite();
|
|
970
|
+
if (uploadError)
|
|
971
|
+
throw uploadError;
|
|
972
|
+
if (!fileFound || !result) {
|
|
973
|
+
throw Object.assign(new Error("no file uploaded"), { status: 400 });
|
|
974
|
+
}
|
|
975
|
+
if (result.size <= 0) {
|
|
976
|
+
throw Object.assign(new Error("empty upload"), { status: 400 });
|
|
977
|
+
}
|
|
978
|
+
localPathToCleanup = null;
|
|
979
|
+
return { ...result, withinCodexCwd };
|
|
980
|
+
}
|
|
981
|
+
catch (err) {
|
|
982
|
+
if (localPathToCleanup) {
|
|
983
|
+
try {
|
|
984
|
+
await fs_1.default.promises.unlink(localPathToCleanup);
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
// ignore
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
throw err;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
//# sourceMappingURL=app.js.map
|