@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.
Files changed (2) hide show
  1. package/dist/main.mjs +446 -16
  2. 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: run.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 { userId, openId, tenantKey } = res.locals.chatUser;
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
- await messageStore.deleteSession(sessionId);
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("列出用户会话", { userId });
7974
- return await this.readUserSessions(userId);
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.71",
3
+ "version": "0.1.73",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",