@vibe-lark/larkpal 0.1.72 → 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 +439 -15
- 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 格式数据到响应流 */
|
|
@@ -3771,6 +3778,31 @@ function getRuntimeEventDedupeKey(event) {
|
|
|
3771
3778
|
function isRunVisibleToUser(run, params) {
|
|
3772
3779
|
return run.tenantKey === params.tenantKey && run.openId === params.openId;
|
|
3773
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
|
+
}
|
|
3774
3806
|
function addRunClient(req, res, run) {
|
|
3775
3807
|
run.clients.add(res);
|
|
3776
3808
|
req.on("close", () => {
|
|
@@ -4009,6 +4041,209 @@ function buildRuntimeEventMessageMetadata(event, runtimeConfig) {
|
|
|
4009
4041
|
finalStatus: event.finalStatus
|
|
4010
4042
|
};
|
|
4011
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
|
+
}
|
|
4012
4247
|
/**
|
|
4013
4248
|
* 创建 Chat 流式对话 + 历史查询路由
|
|
4014
4249
|
*
|
|
@@ -4025,7 +4260,8 @@ function createChatRouter(config) {
|
|
|
4025
4260
|
const router = Router();
|
|
4026
4261
|
const { processManager, messageStore } = config;
|
|
4027
4262
|
router.post("/api/chat/stream", chatAuthMiddleware, async (req, res) => {
|
|
4028
|
-
const
|
|
4263
|
+
const chatUser = res.locals.chatUser;
|
|
4264
|
+
const { userId, openId, tenantKey } = chatUser;
|
|
4029
4265
|
const body = req.body;
|
|
4030
4266
|
if (!tenantKey || !userId || !openId) {
|
|
4031
4267
|
res.status(401).json({ error: "Missing required chat identity: tenantKey, userId, openId" });
|
|
@@ -4034,7 +4270,48 @@ function createChatRouter(config) {
|
|
|
4034
4270
|
const requestId = getGatewayRequestId(req.headers);
|
|
4035
4271
|
const traceId = getGatewayTraceId(req.headers, requestId);
|
|
4036
4272
|
let sessionId = body.session_id;
|
|
4273
|
+
let streamSession = null;
|
|
4037
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
|
+
}
|
|
4038
4315
|
if (activeRun) {
|
|
4039
4316
|
if (!isRunVisibleToUser(activeRun, {
|
|
4040
4317
|
tenantKey,
|
|
@@ -4057,6 +4334,22 @@ function createChatRouter(config) {
|
|
|
4057
4334
|
res.status(400).json({ error: "prompt is required and must be a string" });
|
|
4058
4335
|
return;
|
|
4059
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
|
+
}
|
|
4060
4353
|
const scenarioId = getScenarioId(body.options?.scenarioId, body.options?.metadata);
|
|
4061
4354
|
const attachments = getRunInputValue(body.attachments, body.options?.attachments, body.options?.metadata?.attachments);
|
|
4062
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);
|
|
@@ -4077,6 +4370,14 @@ function createChatRouter(config) {
|
|
|
4077
4370
|
tenantKey,
|
|
4078
4371
|
channel: "web"
|
|
4079
4372
|
})).id;
|
|
4373
|
+
logSessionDecision({
|
|
4374
|
+
action: "create",
|
|
4375
|
+
decision: "allowed",
|
|
4376
|
+
chatUser,
|
|
4377
|
+
sessionId,
|
|
4378
|
+
requestId,
|
|
4379
|
+
traceId
|
|
4380
|
+
});
|
|
4080
4381
|
log$30.info("[stream] 自动创建新会话", {
|
|
4081
4382
|
userId: openId,
|
|
4082
4383
|
sessionId
|
|
@@ -4559,6 +4860,29 @@ function createChatRouter(config) {
|
|
|
4559
4860
|
router.post("/api/chat/sessions/:id/cancel-active-run", chatAuthMiddleware, async (req, res) => {
|
|
4560
4861
|
const chatUser = res.locals.chatUser;
|
|
4561
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
|
+
});
|
|
4562
4886
|
const run = chatRunsBySession.get(sessionId);
|
|
4563
4887
|
if (!run) {
|
|
4564
4888
|
res.json({
|
|
@@ -4596,6 +4920,9 @@ function createChatRouter(config) {
|
|
|
4596
4920
|
});
|
|
4597
4921
|
}
|
|
4598
4922
|
});
|
|
4923
|
+
router.post("/api/chat/sessions/:id/files", chatAuthMiddleware, async (req, res) => {
|
|
4924
|
+
await handleSessionFileUpload(req, res, messageStore);
|
|
4925
|
+
});
|
|
4599
4926
|
router.get("/api/chat/history", chatAuthMiddleware, async (req, res) => {
|
|
4600
4927
|
const chatUser = res.locals.chatUser;
|
|
4601
4928
|
const sessionId = req.query.session_id;
|
|
@@ -4605,6 +4932,8 @@ function createChatRouter(config) {
|
|
|
4605
4932
|
}
|
|
4606
4933
|
const cursor = req.query.cursor;
|
|
4607
4934
|
const limit = parseInt(req.query.limit, 10) || 50;
|
|
4935
|
+
const requestId = getGatewayRequestId(req.headers);
|
|
4936
|
+
const traceId = getGatewayTraceId(req.headers, requestId);
|
|
4608
4937
|
log$30.info("[history] 查询会话历史", {
|
|
4609
4938
|
userId: chatUser.userId,
|
|
4610
4939
|
sessionId,
|
|
@@ -4612,6 +4941,27 @@ function createChatRouter(config) {
|
|
|
4612
4941
|
limit
|
|
4613
4942
|
});
|
|
4614
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
|
+
});
|
|
4615
4965
|
const result = await messageStore.getMessages(sessionId, {
|
|
4616
4966
|
cursor,
|
|
4617
4967
|
limit
|
|
@@ -4647,6 +4997,29 @@ function createChatRouter(config) {
|
|
|
4647
4997
|
res.status(400).json({ error: "session_id is required" });
|
|
4648
4998
|
return;
|
|
4649
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
|
+
});
|
|
4650
5023
|
const run = chatRunsBySession.get(sessionId);
|
|
4651
5024
|
if (!run || !isRunVisibleToUser(run, {
|
|
4652
5025
|
tenantKey: chatUser.tenantKey,
|
|
@@ -4673,7 +5046,7 @@ function createChatRouter(config) {
|
|
|
4673
5046
|
const chatUser = res.locals.chatUser;
|
|
4674
5047
|
log$30.info("[sessions] 列出用户会话", { userId: chatUser.userId });
|
|
4675
5048
|
try {
|
|
4676
|
-
const sessions = await messageStore.listSessions(chatUser.openId);
|
|
5049
|
+
const sessions = await messageStore.listSessions(chatUser.openId, chatUser.tenantKey);
|
|
4677
5050
|
res.json({ sessions });
|
|
4678
5051
|
} catch (err) {
|
|
4679
5052
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -4717,7 +5090,29 @@ function createChatRouter(config) {
|
|
|
4717
5090
|
sessionId
|
|
4718
5091
|
});
|
|
4719
5092
|
try {
|
|
4720
|
-
|
|
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
|
+
});
|
|
4721
5116
|
chatRunsBySession.delete(sessionId);
|
|
4722
5117
|
res.json({ success: true });
|
|
4723
5118
|
} catch (err) {
|
|
@@ -7935,6 +8330,11 @@ var SessionMessageStore = class {
|
|
|
7935
8330
|
* 创建新会话
|
|
7936
8331
|
*/
|
|
7937
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
|
+
}
|
|
7938
8338
|
const now = Date.now();
|
|
7939
8339
|
const fullSession = {
|
|
7940
8340
|
...session,
|
|
@@ -7946,9 +8346,9 @@ var SessionMessageStore = class {
|
|
|
7946
8346
|
sessionId: fullSession.id,
|
|
7947
8347
|
userId: fullSession.userId
|
|
7948
8348
|
});
|
|
7949
|
-
const sessions = await this.readUserSessions(fullSession.userId);
|
|
8349
|
+
const sessions = await this.readUserSessions(fullSession.userId, fullSession.tenantKey);
|
|
7950
8350
|
sessions.push(fullSession);
|
|
7951
|
-
await this.writeUserSessions(fullSession.userId, sessions);
|
|
8351
|
+
await this.writeUserSessions(fullSession.userId, sessions, fullSession.tenantKey);
|
|
7952
8352
|
return fullSession;
|
|
7953
8353
|
}
|
|
7954
8354
|
/**
|
|
@@ -7975,9 +8375,21 @@ var SessionMessageStore = class {
|
|
|
7975
8375
|
/**
|
|
7976
8376
|
* 列出指定用户的所有会话
|
|
7977
8377
|
*/
|
|
7978
|
-
async listSessions(userId) {
|
|
7979
|
-
log$24.debug("列出用户会话", {
|
|
7980
|
-
|
|
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;
|
|
7981
8393
|
}
|
|
7982
8394
|
/**
|
|
7983
8395
|
* 删除指定会话(同时删除消息文件)
|
|
@@ -8007,6 +8419,12 @@ var SessionMessageStore = class {
|
|
|
8007
8419
|
throw err;
|
|
8008
8420
|
}
|
|
8009
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
|
+
}
|
|
8010
8428
|
/**
|
|
8011
8429
|
* 追加消息到会话
|
|
8012
8430
|
* 同时更新会话的 updatedAt 和 messageCount
|
|
@@ -8096,21 +8514,21 @@ var SessionMessageStore = class {
|
|
|
8096
8514
|
/**
|
|
8097
8515
|
* 获取用户会话索引文件路径
|
|
8098
8516
|
*/
|
|
8099
|
-
getUserSessionFilePath(userId) {
|
|
8100
|
-
return join(this.sessionsDir, `${userId}.json`);
|
|
8517
|
+
getUserSessionFilePath(userId, tenantKey) {
|
|
8518
|
+
return join(this.sessionsDir, `${buildOwnerKey(userId, tenantKey)}.json`);
|
|
8101
8519
|
}
|
|
8102
8520
|
/**
|
|
8103
8521
|
* 读取用户的会话列表
|
|
8104
8522
|
*/
|
|
8105
|
-
async readUserSessions(userId) {
|
|
8106
|
-
const filePath = this.getUserSessionFilePath(userId);
|
|
8523
|
+
async readUserSessions(userId, tenantKey) {
|
|
8524
|
+
const filePath = this.getUserSessionFilePath(userId, tenantKey);
|
|
8107
8525
|
return await this.readJsonFile(filePath) || [];
|
|
8108
8526
|
}
|
|
8109
8527
|
/**
|
|
8110
8528
|
* 写入用户的会话列表
|
|
8111
8529
|
*/
|
|
8112
|
-
async writeUserSessions(userId, sessions) {
|
|
8113
|
-
const filePath = this.getUserSessionFilePath(userId);
|
|
8530
|
+
async writeUserSessions(userId, sessions, tenantKey) {
|
|
8531
|
+
const filePath = this.getUserSessionFilePath(userId, tenantKey);
|
|
8114
8532
|
await this.writeJsonFile(filePath, sessions);
|
|
8115
8533
|
}
|
|
8116
8534
|
/**
|
|
@@ -8210,6 +8628,12 @@ var SessionMessageStore = class {
|
|
|
8210
8628
|
} catch {}
|
|
8211
8629
|
}
|
|
8212
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
|
+
}
|
|
8213
8637
|
//#endregion
|
|
8214
8638
|
//#region src/messaging/inbound/user-name-cache-store.ts
|
|
8215
8639
|
/**
|