@vibe-lark/larkpal 0.1.71 → 0.1.73
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/main.mjs +446 -16
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { n as getLarkRuntime, r as setLarkRuntime, t as larkLogger } from "./lar
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import * as path$2 from "node:path";
|
|
6
|
-
import path, { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
|
+
import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import "dotenv/config";
|
|
9
9
|
import { access, copyFile, lstat, mkdir, open, readFile, readdir, readlink, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
@@ -3679,6 +3679,13 @@ function summarizeString(value) {
|
|
|
3679
3679
|
* DELETE /api/chat/sessions/:id — 删除会话
|
|
3680
3680
|
*/
|
|
3681
3681
|
const log$30 = larkLogger("chat/stream");
|
|
3682
|
+
var SessionFileUploadError = class extends Error {
|
|
3683
|
+
constructor(message, statusCode = 400) {
|
|
3684
|
+
super(message);
|
|
3685
|
+
this.statusCode = statusCode;
|
|
3686
|
+
this.name = "SessionFileUploadError";
|
|
3687
|
+
}
|
|
3688
|
+
};
|
|
3682
3689
|
const chatRunsBySession = /* @__PURE__ */ new Map();
|
|
3683
3690
|
const DEFAULT_STALE_RUN_IDLE_MS = 1800 * 1e3;
|
|
3684
3691
|
/** 统一写 SSE 格式数据到响应流 */
|
|
@@ -3708,6 +3715,7 @@ function findChatRunByRunId(runId) {
|
|
|
3708
3715
|
}
|
|
3709
3716
|
function serializeChatRun(run) {
|
|
3710
3717
|
const idleSeconds = Math.max(0, Math.floor((Date.now() - run.lastEventAt) / 1e3));
|
|
3718
|
+
const processStatus = getSerializableRunProcessStatus(run);
|
|
3711
3719
|
return {
|
|
3712
3720
|
runId: run.runId,
|
|
3713
3721
|
sessionId: run.sessionId,
|
|
@@ -3726,10 +3734,15 @@ function serializeChatRun(run) {
|
|
|
3726
3734
|
lastToolName: run.lastToolName,
|
|
3727
3735
|
idleSeconds,
|
|
3728
3736
|
adapterBusy: run.status === "running" ? run.adapterBusy : false,
|
|
3729
|
-
processStatus
|
|
3737
|
+
processStatus,
|
|
3730
3738
|
staleReason: run.staleReason
|
|
3731
3739
|
};
|
|
3732
3740
|
}
|
|
3741
|
+
function getSerializableRunProcessStatus(run) {
|
|
3742
|
+
if (run.status === "running") return run.processStatus;
|
|
3743
|
+
if (run.processStatus === "running" || run.processStatus === "starting") return void 0;
|
|
3744
|
+
return run.processStatus;
|
|
3745
|
+
}
|
|
3733
3746
|
function serializeRuntimeProcessInfo(processInfo, processManager) {
|
|
3734
3747
|
return {
|
|
3735
3748
|
...processInfo,
|
|
@@ -3765,6 +3778,31 @@ function getRuntimeEventDedupeKey(event) {
|
|
|
3765
3778
|
function isRunVisibleToUser(run, params) {
|
|
3766
3779
|
return run.tenantKey === params.tenantKey && run.openId === params.openId;
|
|
3767
3780
|
}
|
|
3781
|
+
function getSessionOwner(chatUser) {
|
|
3782
|
+
return {
|
|
3783
|
+
tenantKey: chatUser.tenantKey,
|
|
3784
|
+
openId: chatUser.openId
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
function isSessionOwnedBy$1(session, owner) {
|
|
3788
|
+
return session.tenantKey === owner.tenantKey && session.userId === owner.openId;
|
|
3789
|
+
}
|
|
3790
|
+
async function getOwnedSession(messageStore, sessionId, chatUser) {
|
|
3791
|
+
return await messageStore.getSessionForOwner(sessionId, getSessionOwner(chatUser));
|
|
3792
|
+
}
|
|
3793
|
+
function logSessionDecision(params) {
|
|
3794
|
+
log$30.info("[session-audit] chat session access decision", {
|
|
3795
|
+
action: params.action,
|
|
3796
|
+
decision: params.decision,
|
|
3797
|
+
tenantKey: params.chatUser.tenantKey,
|
|
3798
|
+
userId: params.chatUser.userId,
|
|
3799
|
+
openId: params.chatUser.openId,
|
|
3800
|
+
sessionId: params.sessionId,
|
|
3801
|
+
requestId: params.requestId,
|
|
3802
|
+
traceId: params.traceId,
|
|
3803
|
+
reason: params.reason
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3768
3806
|
function addRunClient(req, res, run) {
|
|
3769
3807
|
run.clients.add(res);
|
|
3770
3808
|
req.on("close", () => {
|
|
@@ -4003,6 +4041,209 @@ function buildRuntimeEventMessageMetadata(event, runtimeConfig) {
|
|
|
4003
4041
|
finalStatus: event.finalStatus
|
|
4004
4042
|
};
|
|
4005
4043
|
}
|
|
4044
|
+
async function handleSessionFileUpload(req, res, messageStore) {
|
|
4045
|
+
const chatUser = res.locals.chatUser;
|
|
4046
|
+
const sessionId = String(req.params.id);
|
|
4047
|
+
const requestId = getGatewayRequestId(req.headers);
|
|
4048
|
+
const traceId = getGatewayTraceId(req.headers, requestId);
|
|
4049
|
+
if (!await getOwnedSession(messageStore, sessionId, chatUser)) {
|
|
4050
|
+
logSessionDecision({
|
|
4051
|
+
action: "attach",
|
|
4052
|
+
decision: "denied",
|
|
4053
|
+
chatUser,
|
|
4054
|
+
sessionId,
|
|
4055
|
+
requestId,
|
|
4056
|
+
traceId,
|
|
4057
|
+
reason: "session_not_owned"
|
|
4058
|
+
});
|
|
4059
|
+
res.status(404).json({ error: "Session not found" });
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
logSessionDecision({
|
|
4063
|
+
action: "attach",
|
|
4064
|
+
decision: "allowed",
|
|
4065
|
+
chatUser,
|
|
4066
|
+
sessionId,
|
|
4067
|
+
requestId,
|
|
4068
|
+
traceId
|
|
4069
|
+
});
|
|
4070
|
+
if (!req.headers["content-type"]?.includes("multipart/form-data")) {
|
|
4071
|
+
res.status(415).json({ error: "Content-Type must be multipart/form-data" });
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
try {
|
|
4075
|
+
const file = await parseSessionFileUpload(req, sessionId);
|
|
4076
|
+
await messageStore.appendMessage({
|
|
4077
|
+
sessionId,
|
|
4078
|
+
role: "tool",
|
|
4079
|
+
content: JSON.stringify({
|
|
4080
|
+
runtimeEventType: "session-file-attached",
|
|
4081
|
+
file
|
|
4082
|
+
}),
|
|
4083
|
+
channel: "web",
|
|
4084
|
+
metadata: {
|
|
4085
|
+
requestId,
|
|
4086
|
+
traceId,
|
|
4087
|
+
runtimeEventType: "session-file-attached",
|
|
4088
|
+
fileName: file.fileName,
|
|
4089
|
+
relativePath: file.relativePath,
|
|
4090
|
+
mimeType: file.mimeType,
|
|
4091
|
+
size: file.size,
|
|
4092
|
+
attachments: [file]
|
|
4093
|
+
}
|
|
4094
|
+
});
|
|
4095
|
+
res.status(201).json({
|
|
4096
|
+
file,
|
|
4097
|
+
files: [file]
|
|
4098
|
+
});
|
|
4099
|
+
} catch (err) {
|
|
4100
|
+
const statusCode = err instanceof SessionFileUploadError ? err.statusCode : 500;
|
|
4101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4102
|
+
log$30.warn("[sessions] session file upload failed", {
|
|
4103
|
+
tenantKey: chatUser.tenantKey,
|
|
4104
|
+
userId: chatUser.userId,
|
|
4105
|
+
openId: chatUser.openId,
|
|
4106
|
+
sessionId,
|
|
4107
|
+
requestId,
|
|
4108
|
+
traceId,
|
|
4109
|
+
statusCode,
|
|
4110
|
+
message
|
|
4111
|
+
});
|
|
4112
|
+
res.status(statusCode).json({ error: message });
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
function parseSessionFileUpload(req, sessionId) {
|
|
4116
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
4117
|
+
const busboy = Busboy({
|
|
4118
|
+
headers: req.headers,
|
|
4119
|
+
limits: {
|
|
4120
|
+
fileSize: getSessionFileUploadMaxBytes(),
|
|
4121
|
+
files: 1
|
|
4122
|
+
}
|
|
4123
|
+
});
|
|
4124
|
+
let metadata;
|
|
4125
|
+
let uploadPromise;
|
|
4126
|
+
let fileSeen = false;
|
|
4127
|
+
let settled = false;
|
|
4128
|
+
const fail = (error) => {
|
|
4129
|
+
if (settled) return;
|
|
4130
|
+
settled = true;
|
|
4131
|
+
rejectPromise(error);
|
|
4132
|
+
};
|
|
4133
|
+
busboy.on("field", (name, value) => {
|
|
4134
|
+
if (name !== "metadata") return;
|
|
4135
|
+
if (!value.trim()) return;
|
|
4136
|
+
try {
|
|
4137
|
+
const parsed = JSON.parse(value);
|
|
4138
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
4139
|
+
metadata = parsed;
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
fail(new SessionFileUploadError("metadata must be a JSON object"));
|
|
4143
|
+
} catch (err) {
|
|
4144
|
+
fail(new SessionFileUploadError(`metadata is not valid JSON: ${err instanceof Error ? err.message : String(err)}`));
|
|
4145
|
+
}
|
|
4146
|
+
});
|
|
4147
|
+
busboy.on("file", (_name, file, info) => {
|
|
4148
|
+
if (fileSeen) {
|
|
4149
|
+
file.resume();
|
|
4150
|
+
fail(new SessionFileUploadError("Only one file is supported per upload"));
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
fileSeen = true;
|
|
4154
|
+
uploadPromise = writeSessionFile(sessionId, {
|
|
4155
|
+
file,
|
|
4156
|
+
originalName: info.filename || "upload.bin",
|
|
4157
|
+
mimeType: info.mimeType || "application/octet-stream",
|
|
4158
|
+
metadata
|
|
4159
|
+
});
|
|
4160
|
+
});
|
|
4161
|
+
busboy.on("filesLimit", () => fail(new SessionFileUploadError("Only one file is supported per upload")));
|
|
4162
|
+
busboy.on("error", fail);
|
|
4163
|
+
busboy.on("finish", () => {
|
|
4164
|
+
if (settled) return;
|
|
4165
|
+
if (!fileSeen || !uploadPromise) {
|
|
4166
|
+
fail(new SessionFileUploadError("file is required"));
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
uploadPromise.then((file) => {
|
|
4170
|
+
if (settled) return;
|
|
4171
|
+
settled = true;
|
|
4172
|
+
resolvePromise(file);
|
|
4173
|
+
}, fail);
|
|
4174
|
+
});
|
|
4175
|
+
req.pipe(busboy);
|
|
4176
|
+
});
|
|
4177
|
+
}
|
|
4178
|
+
function writeSessionFile(sessionId, params) {
|
|
4179
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
4180
|
+
const uploadsDir = resolve(resolveAgentCwd(sessionId), "uploads");
|
|
4181
|
+
const fileName = buildSafeUploadFileName(params.originalName);
|
|
4182
|
+
const destination = resolve(uploadsDir, fileName);
|
|
4183
|
+
const relativePath = `uploads/${fileName}`;
|
|
4184
|
+
const maxBytes = getSessionFileUploadMaxBytes();
|
|
4185
|
+
let size = 0;
|
|
4186
|
+
let settled = false;
|
|
4187
|
+
const fail = (error) => {
|
|
4188
|
+
if (settled) return;
|
|
4189
|
+
settled = true;
|
|
4190
|
+
rejectPromise(error);
|
|
4191
|
+
};
|
|
4192
|
+
if (!isPathInside(uploadsDir, destination)) {
|
|
4193
|
+
fail(new SessionFileUploadError("Invalid upload path"));
|
|
4194
|
+
params.file.resume();
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
mkdir(uploadsDir, { recursive: true }).then(() => {
|
|
4198
|
+
const output = createWriteStream(destination, { flags: "wx" });
|
|
4199
|
+
params.file.on("data", (chunk) => {
|
|
4200
|
+
size += chunk.length;
|
|
4201
|
+
if (size > maxBytes) {
|
|
4202
|
+
output.destroy();
|
|
4203
|
+
fail(new SessionFileUploadError(`File exceeds maximum upload size of ${maxBytes} bytes`, 413));
|
|
4204
|
+
}
|
|
4205
|
+
});
|
|
4206
|
+
params.file.on("limit", () => {
|
|
4207
|
+
output.destroy();
|
|
4208
|
+
fail(new SessionFileUploadError(`File exceeds maximum upload size of ${maxBytes} bytes`, 413));
|
|
4209
|
+
});
|
|
4210
|
+
params.file.on("error", fail);
|
|
4211
|
+
output.on("error", fail);
|
|
4212
|
+
output.on("finish", () => {
|
|
4213
|
+
if (settled) return;
|
|
4214
|
+
settled = true;
|
|
4215
|
+
resolvePromise({
|
|
4216
|
+
sessionId,
|
|
4217
|
+
kind: "session-file",
|
|
4218
|
+
fileName,
|
|
4219
|
+
relativePath,
|
|
4220
|
+
uri: `session-file://${relativePath}`,
|
|
4221
|
+
mimeType: params.mimeType,
|
|
4222
|
+
size,
|
|
4223
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4224
|
+
metadata: params.metadata
|
|
4225
|
+
});
|
|
4226
|
+
});
|
|
4227
|
+
params.file.pipe(output);
|
|
4228
|
+
}, fail);
|
|
4229
|
+
});
|
|
4230
|
+
}
|
|
4231
|
+
function getSessionFileUploadMaxBytes() {
|
|
4232
|
+
const raw = process.env.LARKPAL_SESSION_FILE_MAX_UPLOAD_BYTES || process.env.LARKPAL_ARTIFACT_MAX_UPLOAD_BYTES;
|
|
4233
|
+
if (!raw) return 50 * 1024 * 1024;
|
|
4234
|
+
const parsed = Number.parseInt(raw, 10);
|
|
4235
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 50 * 1024 * 1024;
|
|
4236
|
+
}
|
|
4237
|
+
function buildSafeUploadFileName(originalName) {
|
|
4238
|
+
const base = basename(originalName).replace(/[\0\r\n\\/]/g, "_");
|
|
4239
|
+
const extension = extname(base).replace(/[^A-Za-z0-9.]/g, "").slice(0, 32);
|
|
4240
|
+
const stem = basename(base, extname(base)).replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+$/, "").slice(0, 80) || "upload";
|
|
4241
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${stem}${extension}`;
|
|
4242
|
+
}
|
|
4243
|
+
function isPathInside(parent, child) {
|
|
4244
|
+
const path = relative(parent, child);
|
|
4245
|
+
return path !== "" && !path.startsWith("..") && !isAbsolute(path);
|
|
4246
|
+
}
|
|
4006
4247
|
/**
|
|
4007
4248
|
* 创建 Chat 流式对话 + 历史查询路由
|
|
4008
4249
|
*
|
|
@@ -4019,7 +4260,8 @@ function createChatRouter(config) {
|
|
|
4019
4260
|
const router = Router();
|
|
4020
4261
|
const { processManager, messageStore } = config;
|
|
4021
4262
|
router.post("/api/chat/stream", chatAuthMiddleware, async (req, res) => {
|
|
4022
|
-
const
|
|
4263
|
+
const chatUser = res.locals.chatUser;
|
|
4264
|
+
const { userId, openId, tenantKey } = chatUser;
|
|
4023
4265
|
const body = req.body;
|
|
4024
4266
|
if (!tenantKey || !userId || !openId) {
|
|
4025
4267
|
res.status(401).json({ error: "Missing required chat identity: tenantKey, userId, openId" });
|
|
@@ -4028,7 +4270,48 @@ function createChatRouter(config) {
|
|
|
4028
4270
|
const requestId = getGatewayRequestId(req.headers);
|
|
4029
4271
|
const traceId = getGatewayTraceId(req.headers, requestId);
|
|
4030
4272
|
let sessionId = body.session_id;
|
|
4273
|
+
let streamSession = null;
|
|
4031
4274
|
const activeRun = sessionId ? getActiveChatRun(sessionId) : void 0;
|
|
4275
|
+
if (sessionId) {
|
|
4276
|
+
streamSession = await messageStore.getSession(sessionId);
|
|
4277
|
+
if (streamSession && !isSessionOwnedBy$1(streamSession, getSessionOwner(chatUser))) {
|
|
4278
|
+
logSessionDecision({
|
|
4279
|
+
action: "stream",
|
|
4280
|
+
decision: "denied",
|
|
4281
|
+
chatUser,
|
|
4282
|
+
sessionId,
|
|
4283
|
+
requestId,
|
|
4284
|
+
traceId,
|
|
4285
|
+
reason: "session_not_owned"
|
|
4286
|
+
});
|
|
4287
|
+
res.status(404).json({ error: "Session not found" });
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
if (!streamSession && activeRun && !isRunVisibleToUser(activeRun, {
|
|
4291
|
+
tenantKey,
|
|
4292
|
+
openId
|
|
4293
|
+
})) {
|
|
4294
|
+
logSessionDecision({
|
|
4295
|
+
action: "stream",
|
|
4296
|
+
decision: "denied",
|
|
4297
|
+
chatUser,
|
|
4298
|
+
sessionId,
|
|
4299
|
+
requestId,
|
|
4300
|
+
traceId,
|
|
4301
|
+
reason: "active_run_not_owned"
|
|
4302
|
+
});
|
|
4303
|
+
res.status(404).json({ error: "Session not found" });
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
if (streamSession || activeRun) logSessionDecision({
|
|
4307
|
+
action: "stream",
|
|
4308
|
+
decision: "allowed",
|
|
4309
|
+
chatUser,
|
|
4310
|
+
sessionId,
|
|
4311
|
+
requestId,
|
|
4312
|
+
traceId
|
|
4313
|
+
});
|
|
4314
|
+
}
|
|
4032
4315
|
if (activeRun) {
|
|
4033
4316
|
if (!isRunVisibleToUser(activeRun, {
|
|
4034
4317
|
tenantKey,
|
|
@@ -4051,6 +4334,22 @@ function createChatRouter(config) {
|
|
|
4051
4334
|
res.status(400).json({ error: "prompt is required and must be a string" });
|
|
4052
4335
|
return;
|
|
4053
4336
|
}
|
|
4337
|
+
if (sessionId && !streamSession) {
|
|
4338
|
+
await messageStore.createSession({
|
|
4339
|
+
id: sessionId,
|
|
4340
|
+
userId: openId,
|
|
4341
|
+
tenantKey,
|
|
4342
|
+
channel: "web"
|
|
4343
|
+
});
|
|
4344
|
+
logSessionDecision({
|
|
4345
|
+
action: "stream",
|
|
4346
|
+
decision: "allowed",
|
|
4347
|
+
chatUser,
|
|
4348
|
+
sessionId,
|
|
4349
|
+
requestId,
|
|
4350
|
+
traceId
|
|
4351
|
+
});
|
|
4352
|
+
}
|
|
4054
4353
|
const scenarioId = getScenarioId(body.options?.scenarioId, body.options?.metadata);
|
|
4055
4354
|
const attachments = getRunInputValue(body.attachments, body.options?.attachments, body.options?.metadata?.attachments);
|
|
4056
4355
|
const sourceArtifacts = getRunInputValue(body.sourceArtifacts, body.source_artifacts, body.options?.sourceArtifacts, body.options?.source_artifacts, body.options?.metadata?.sourceArtifacts, body.options?.metadata?.source_artifacts);
|
|
@@ -4071,6 +4370,14 @@ function createChatRouter(config) {
|
|
|
4071
4370
|
tenantKey,
|
|
4072
4371
|
channel: "web"
|
|
4073
4372
|
})).id;
|
|
4373
|
+
logSessionDecision({
|
|
4374
|
+
action: "create",
|
|
4375
|
+
decision: "allowed",
|
|
4376
|
+
chatUser,
|
|
4377
|
+
sessionId,
|
|
4378
|
+
requestId,
|
|
4379
|
+
traceId
|
|
4380
|
+
});
|
|
4074
4381
|
log$30.info("[stream] 自动创建新会话", {
|
|
4075
4382
|
userId: openId,
|
|
4076
4383
|
sessionId
|
|
@@ -4553,6 +4860,29 @@ function createChatRouter(config) {
|
|
|
4553
4860
|
router.post("/api/chat/sessions/:id/cancel-active-run", chatAuthMiddleware, async (req, res) => {
|
|
4554
4861
|
const chatUser = res.locals.chatUser;
|
|
4555
4862
|
const sessionId = String(req.params.id);
|
|
4863
|
+
const requestId = getGatewayRequestId(req.headers);
|
|
4864
|
+
const traceId = getGatewayTraceId(req.headers, requestId);
|
|
4865
|
+
if (!await getOwnedSession(messageStore, sessionId, chatUser)) {
|
|
4866
|
+
logSessionDecision({
|
|
4867
|
+
action: "cancel",
|
|
4868
|
+
decision: "denied",
|
|
4869
|
+
chatUser,
|
|
4870
|
+
sessionId,
|
|
4871
|
+
requestId,
|
|
4872
|
+
traceId,
|
|
4873
|
+
reason: "session_not_owned"
|
|
4874
|
+
});
|
|
4875
|
+
res.status(404).json({ error: "Session not found" });
|
|
4876
|
+
return;
|
|
4877
|
+
}
|
|
4878
|
+
logSessionDecision({
|
|
4879
|
+
action: "cancel",
|
|
4880
|
+
decision: "allowed",
|
|
4881
|
+
chatUser,
|
|
4882
|
+
sessionId,
|
|
4883
|
+
requestId,
|
|
4884
|
+
traceId
|
|
4885
|
+
});
|
|
4556
4886
|
const run = chatRunsBySession.get(sessionId);
|
|
4557
4887
|
if (!run) {
|
|
4558
4888
|
res.json({
|
|
@@ -4590,6 +4920,9 @@ function createChatRouter(config) {
|
|
|
4590
4920
|
});
|
|
4591
4921
|
}
|
|
4592
4922
|
});
|
|
4923
|
+
router.post("/api/chat/sessions/:id/files", chatAuthMiddleware, async (req, res) => {
|
|
4924
|
+
await handleSessionFileUpload(req, res, messageStore);
|
|
4925
|
+
});
|
|
4593
4926
|
router.get("/api/chat/history", chatAuthMiddleware, async (req, res) => {
|
|
4594
4927
|
const chatUser = res.locals.chatUser;
|
|
4595
4928
|
const sessionId = req.query.session_id;
|
|
@@ -4599,6 +4932,8 @@ function createChatRouter(config) {
|
|
|
4599
4932
|
}
|
|
4600
4933
|
const cursor = req.query.cursor;
|
|
4601
4934
|
const limit = parseInt(req.query.limit, 10) || 50;
|
|
4935
|
+
const requestId = getGatewayRequestId(req.headers);
|
|
4936
|
+
const traceId = getGatewayTraceId(req.headers, requestId);
|
|
4602
4937
|
log$30.info("[history] 查询会话历史", {
|
|
4603
4938
|
userId: chatUser.userId,
|
|
4604
4939
|
sessionId,
|
|
@@ -4606,6 +4941,27 @@ function createChatRouter(config) {
|
|
|
4606
4941
|
limit
|
|
4607
4942
|
});
|
|
4608
4943
|
try {
|
|
4944
|
+
if (!await getOwnedSession(messageStore, sessionId, chatUser)) {
|
|
4945
|
+
logSessionDecision({
|
|
4946
|
+
action: "history",
|
|
4947
|
+
decision: "denied",
|
|
4948
|
+
chatUser,
|
|
4949
|
+
sessionId,
|
|
4950
|
+
requestId,
|
|
4951
|
+
traceId,
|
|
4952
|
+
reason: "session_not_owned"
|
|
4953
|
+
});
|
|
4954
|
+
res.status(404).json({ error: "Session not found" });
|
|
4955
|
+
return;
|
|
4956
|
+
}
|
|
4957
|
+
logSessionDecision({
|
|
4958
|
+
action: "history",
|
|
4959
|
+
decision: "allowed",
|
|
4960
|
+
chatUser,
|
|
4961
|
+
sessionId,
|
|
4962
|
+
requestId,
|
|
4963
|
+
traceId
|
|
4964
|
+
});
|
|
4609
4965
|
const result = await messageStore.getMessages(sessionId, {
|
|
4610
4966
|
cursor,
|
|
4611
4967
|
limit
|
|
@@ -4641,6 +4997,29 @@ function createChatRouter(config) {
|
|
|
4641
4997
|
res.status(400).json({ error: "session_id is required" });
|
|
4642
4998
|
return;
|
|
4643
4999
|
}
|
|
5000
|
+
const requestId = getGatewayRequestId(req.headers);
|
|
5001
|
+
const traceId = getGatewayTraceId(req.headers, requestId);
|
|
5002
|
+
if (!await getOwnedSession(messageStore, sessionId, chatUser)) {
|
|
5003
|
+
logSessionDecision({
|
|
5004
|
+
action: "runs",
|
|
5005
|
+
decision: "denied",
|
|
5006
|
+
chatUser,
|
|
5007
|
+
sessionId,
|
|
5008
|
+
requestId,
|
|
5009
|
+
traceId,
|
|
5010
|
+
reason: "session_not_owned"
|
|
5011
|
+
});
|
|
5012
|
+
res.status(404).json({ error: "Session not found" });
|
|
5013
|
+
return;
|
|
5014
|
+
}
|
|
5015
|
+
logSessionDecision({
|
|
5016
|
+
action: "runs",
|
|
5017
|
+
decision: "allowed",
|
|
5018
|
+
chatUser,
|
|
5019
|
+
sessionId,
|
|
5020
|
+
requestId,
|
|
5021
|
+
traceId
|
|
5022
|
+
});
|
|
4644
5023
|
const run = chatRunsBySession.get(sessionId);
|
|
4645
5024
|
if (!run || !isRunVisibleToUser(run, {
|
|
4646
5025
|
tenantKey: chatUser.tenantKey,
|
|
@@ -4667,7 +5046,7 @@ function createChatRouter(config) {
|
|
|
4667
5046
|
const chatUser = res.locals.chatUser;
|
|
4668
5047
|
log$30.info("[sessions] 列出用户会话", { userId: chatUser.userId });
|
|
4669
5048
|
try {
|
|
4670
|
-
const sessions = await messageStore.listSessions(chatUser.openId);
|
|
5049
|
+
const sessions = await messageStore.listSessions(chatUser.openId, chatUser.tenantKey);
|
|
4671
5050
|
res.json({ sessions });
|
|
4672
5051
|
} catch (err) {
|
|
4673
5052
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -4711,7 +5090,29 @@ function createChatRouter(config) {
|
|
|
4711
5090
|
sessionId
|
|
4712
5091
|
});
|
|
4713
5092
|
try {
|
|
4714
|
-
|
|
5093
|
+
const requestId = getGatewayRequestId(req.headers);
|
|
5094
|
+
const traceId = getGatewayTraceId(req.headers, requestId);
|
|
5095
|
+
if (!await messageStore.deleteSessionForOwner(sessionId, getSessionOwner(chatUser))) {
|
|
5096
|
+
logSessionDecision({
|
|
5097
|
+
action: "delete",
|
|
5098
|
+
decision: "denied",
|
|
5099
|
+
chatUser,
|
|
5100
|
+
sessionId,
|
|
5101
|
+
requestId,
|
|
5102
|
+
traceId,
|
|
5103
|
+
reason: "session_not_owned"
|
|
5104
|
+
});
|
|
5105
|
+
res.status(404).json({ error: "Session not found" });
|
|
5106
|
+
return;
|
|
5107
|
+
}
|
|
5108
|
+
logSessionDecision({
|
|
5109
|
+
action: "delete",
|
|
5110
|
+
decision: "allowed",
|
|
5111
|
+
chatUser,
|
|
5112
|
+
sessionId,
|
|
5113
|
+
requestId,
|
|
5114
|
+
traceId
|
|
5115
|
+
});
|
|
4715
5116
|
chatRunsBySession.delete(sessionId);
|
|
4716
5117
|
res.json({ success: true });
|
|
4717
5118
|
} catch (err) {
|
|
@@ -7929,6 +8330,11 @@ var SessionMessageStore = class {
|
|
|
7929
8330
|
* 创建新会话
|
|
7930
8331
|
*/
|
|
7931
8332
|
async createSession(session) {
|
|
8333
|
+
const existing = await this.getSession(session.id);
|
|
8334
|
+
if (existing) {
|
|
8335
|
+
if (existing.userId === session.userId && existing.tenantKey === session.tenantKey) return existing;
|
|
8336
|
+
throw new Error(`Session ${session.id} already exists for a different owner`);
|
|
8337
|
+
}
|
|
7932
8338
|
const now = Date.now();
|
|
7933
8339
|
const fullSession = {
|
|
7934
8340
|
...session,
|
|
@@ -7940,9 +8346,9 @@ var SessionMessageStore = class {
|
|
|
7940
8346
|
sessionId: fullSession.id,
|
|
7941
8347
|
userId: fullSession.userId
|
|
7942
8348
|
});
|
|
7943
|
-
const sessions = await this.readUserSessions(fullSession.userId);
|
|
8349
|
+
const sessions = await this.readUserSessions(fullSession.userId, fullSession.tenantKey);
|
|
7944
8350
|
sessions.push(fullSession);
|
|
7945
|
-
await this.writeUserSessions(fullSession.userId, sessions);
|
|
8351
|
+
await this.writeUserSessions(fullSession.userId, sessions, fullSession.tenantKey);
|
|
7946
8352
|
return fullSession;
|
|
7947
8353
|
}
|
|
7948
8354
|
/**
|
|
@@ -7969,9 +8375,21 @@ var SessionMessageStore = class {
|
|
|
7969
8375
|
/**
|
|
7970
8376
|
* 列出指定用户的所有会话
|
|
7971
8377
|
*/
|
|
7972
|
-
async listSessions(userId) {
|
|
7973
|
-
log$24.debug("列出用户会话", {
|
|
7974
|
-
|
|
8378
|
+
async listSessions(userId, tenantKey) {
|
|
8379
|
+
log$24.debug("列出用户会话", {
|
|
8380
|
+
userId,
|
|
8381
|
+
tenantKey
|
|
8382
|
+
});
|
|
8383
|
+
const sessions = await this.readUserSessions(userId, tenantKey);
|
|
8384
|
+
if (!tenantKey) return sessions;
|
|
8385
|
+
const ownedLegacySessions = (await this.readUserSessions(userId)).filter((session) => session.userId === userId && session.tenantKey === tenantKey);
|
|
8386
|
+
const seen = new Set(sessions.map((session) => session.id));
|
|
8387
|
+
return [...sessions, ...ownedLegacySessions.filter((session) => !seen.has(session.id))];
|
|
8388
|
+
}
|
|
8389
|
+
async getSessionForOwner(sessionId, owner) {
|
|
8390
|
+
const session = await this.getSession(sessionId);
|
|
8391
|
+
if (!session) return null;
|
|
8392
|
+
return isSessionOwnedBy(session, owner) ? session : null;
|
|
7975
8393
|
}
|
|
7976
8394
|
/**
|
|
7977
8395
|
* 删除指定会话(同时删除消息文件)
|
|
@@ -8001,6 +8419,12 @@ var SessionMessageStore = class {
|
|
|
8001
8419
|
throw err;
|
|
8002
8420
|
}
|
|
8003
8421
|
}
|
|
8422
|
+
async deleteSessionForOwner(sessionId, owner) {
|
|
8423
|
+
const session = await this.getSession(sessionId);
|
|
8424
|
+
if (!session || !isSessionOwnedBy(session, owner)) return false;
|
|
8425
|
+
await this.deleteSession(sessionId);
|
|
8426
|
+
return true;
|
|
8427
|
+
}
|
|
8004
8428
|
/**
|
|
8005
8429
|
* 追加消息到会话
|
|
8006
8430
|
* 同时更新会话的 updatedAt 和 messageCount
|
|
@@ -8090,21 +8514,21 @@ var SessionMessageStore = class {
|
|
|
8090
8514
|
/**
|
|
8091
8515
|
* 获取用户会话索引文件路径
|
|
8092
8516
|
*/
|
|
8093
|
-
getUserSessionFilePath(userId) {
|
|
8094
|
-
return join(this.sessionsDir, `${userId}.json`);
|
|
8517
|
+
getUserSessionFilePath(userId, tenantKey) {
|
|
8518
|
+
return join(this.sessionsDir, `${buildOwnerKey(userId, tenantKey)}.json`);
|
|
8095
8519
|
}
|
|
8096
8520
|
/**
|
|
8097
8521
|
* 读取用户的会话列表
|
|
8098
8522
|
*/
|
|
8099
|
-
async readUserSessions(userId) {
|
|
8100
|
-
const filePath = this.getUserSessionFilePath(userId);
|
|
8523
|
+
async readUserSessions(userId, tenantKey) {
|
|
8524
|
+
const filePath = this.getUserSessionFilePath(userId, tenantKey);
|
|
8101
8525
|
return await this.readJsonFile(filePath) || [];
|
|
8102
8526
|
}
|
|
8103
8527
|
/**
|
|
8104
8528
|
* 写入用户的会话列表
|
|
8105
8529
|
*/
|
|
8106
|
-
async writeUserSessions(userId, sessions) {
|
|
8107
|
-
const filePath = this.getUserSessionFilePath(userId);
|
|
8530
|
+
async writeUserSessions(userId, sessions, tenantKey) {
|
|
8531
|
+
const filePath = this.getUserSessionFilePath(userId, tenantKey);
|
|
8108
8532
|
await this.writeJsonFile(filePath, sessions);
|
|
8109
8533
|
}
|
|
8110
8534
|
/**
|
|
@@ -8204,6 +8628,12 @@ var SessionMessageStore = class {
|
|
|
8204
8628
|
} catch {}
|
|
8205
8629
|
}
|
|
8206
8630
|
};
|
|
8631
|
+
function isSessionOwnedBy(session, owner) {
|
|
8632
|
+
return session.userId === owner.openId && session.tenantKey === owner.tenantKey;
|
|
8633
|
+
}
|
|
8634
|
+
function buildOwnerKey(userId, tenantKey) {
|
|
8635
|
+
return (tenantKey ? `${tenantKey}__${userId}` : userId).replace(/[^A-Za-z0-9_.:-]/g, "_");
|
|
8636
|
+
}
|
|
8207
8637
|
//#endregion
|
|
8208
8638
|
//#region src/messaging/inbound/user-name-cache-store.ts
|
|
8209
8639
|
/**
|