@vibe-lark/larkpal 0.1.72 → 0.1.74

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 +589 -22
  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";
@@ -782,7 +782,10 @@ function getToolDisplayName(toolName) {
782
782
  * - parseError(error, rawLine) — 解析错误(不中断流)
783
783
  */
784
784
  const log$36 = larkLogger("cc-runtime/stream-parser");
785
+ const MAX_TOOL_RESULT_CONTENT_CHARS = 8e3;
785
786
  var CCStreamParser = class extends EventEmitter {
787
+ toolNamesById = /* @__PURE__ */ new Map();
788
+ declaredTools = null;
786
789
  /**
787
790
  * 解析一行 NDJSON 文本
788
791
  *
@@ -830,6 +833,7 @@ var CCStreamParser = class extends EventEmitter {
830
833
  subtype: msg.subtype,
831
834
  detail: JSON.stringify(msg).slice(0, 500)
832
835
  });
836
+ this.handleSystem(msg);
833
837
  this.emit("system", msg);
834
838
  break;
835
839
  default:
@@ -858,6 +862,7 @@ var CCStreamParser = class extends EventEmitter {
858
862
  toolUseId: block?.id
859
863
  });
860
864
  if (block?.type === "tool_use" && block.name) {
865
+ this.recordToolUse(block.name, block.id);
861
866
  log$36.info("工具调用开始", {
862
867
  toolName: block.name,
863
868
  displayName: getToolDisplayName(block.name),
@@ -905,6 +910,7 @@ var CCStreamParser = class extends EventEmitter {
905
910
  });
906
911
  if (Array.isArray(content)) {
907
912
  for (const block of content) if (block.type === "tool_use" && block.name) {
913
+ this.recordToolUse(block.name, block.id);
908
914
  log$36.info("从 assistant 消息提取工具调用", {
909
915
  toolName: block.name,
910
916
  displayName: getToolDisplayName(block.name),
@@ -924,10 +930,46 @@ var CCStreamParser = class extends EventEmitter {
924
930
  const content = msg.message?.content;
925
931
  if (!Array.isArray(content)) return;
926
932
  for (const item of content) if (item.type === "tool_result" && item.tool_use_id) {
927
- log$36.debug("工具结果返回", { toolUseId: item.tool_use_id });
928
- this.emit("toolResult", item.tool_use_id);
933
+ const result = this.buildToolResultSummary(item, msg.toolUseResult);
934
+ log$36.debug("工具结果返回", {
935
+ toolUseId: item.tool_use_id,
936
+ toolName: result.toolName,
937
+ isError: result.isError,
938
+ truncated: result.truncated
939
+ });
940
+ this.emit("toolResult", item.tool_use_id, result);
941
+ }
942
+ }
943
+ handleSystem(msg) {
944
+ if (msg.subtype !== "init" || !Array.isArray(msg.tools)) return;
945
+ this.declaredTools = new Set(msg.tools);
946
+ }
947
+ recordToolUse(toolName, toolUseId) {
948
+ if (toolUseId) this.toolNamesById.set(toolUseId, toolName);
949
+ if (this.declaredTools && !this.declaredTools.has(toolName)) {
950
+ log$36.warn("模型请求了未声明工具", {
951
+ toolName,
952
+ toolUseId
953
+ });
954
+ this.emit("unknownToolUse", toolName, toolUseId);
929
955
  }
930
956
  }
957
+ buildToolResultSummary(item, toolUseResult) {
958
+ const serializedContent = serializeToolResultContent(item.content);
959
+ const truncatedContent = truncateToolResultContent(serializedContent);
960
+ const toolUseResultSummary = summarizeToolUseResult(toolUseResult);
961
+ return {
962
+ toolCallId: item.tool_use_id,
963
+ toolName: this.toolNamesById.get(item.tool_use_id),
964
+ rawType: "tool_result",
965
+ isError: item.is_error,
966
+ content: truncatedContent.content,
967
+ resultSummary: extractToolResultSummary(serializedContent, toolUseResultSummary),
968
+ truncated: truncatedContent.truncated || toolUseResultSummary.truncated,
969
+ originalSize: Math.max(serializedContent.length, toolUseResultSummary.originalSize),
970
+ toolUseResult: toolUseResultSummary.summary
971
+ };
972
+ }
931
973
  /**
932
974
  * 处理 tool_progress 消息 — 工具执行进度
933
975
  */
@@ -959,6 +1001,52 @@ var CCStreamParser = class extends EventEmitter {
959
1001
  });
960
1002
  }
961
1003
  };
1004
+ function serializeToolResultContent(content) {
1005
+ if (typeof content === "string") return content;
1006
+ return safeJsonStringify(content);
1007
+ }
1008
+ function summarizeToolUseResult(value) {
1009
+ if (value === void 0) return {
1010
+ originalSize: 0,
1011
+ truncated: false
1012
+ };
1013
+ const serialized = typeof value === "string" ? value : safeJsonStringify(value);
1014
+ const truncated = truncateToolResultContent(serialized);
1015
+ return {
1016
+ summary: typeof value === "string" ? truncated.content : tryParseJson$1(truncated.content) ?? truncated.content,
1017
+ originalSize: serialized.length,
1018
+ truncated: truncated.truncated
1019
+ };
1020
+ }
1021
+ function truncateToolResultContent(content) {
1022
+ if (content.length <= MAX_TOOL_RESULT_CONTENT_CHARS) return {
1023
+ content,
1024
+ truncated: false
1025
+ };
1026
+ return {
1027
+ content: content.slice(0, MAX_TOOL_RESULT_CONTENT_CHARS),
1028
+ truncated: true
1029
+ };
1030
+ }
1031
+ function extractToolResultSummary(content, toolUseResult) {
1032
+ const text = (typeof toolUseResult.summary === "string" ? toolUseResult.summary : toolUseResult.summary === void 0 ? void 0 : safeJsonStringify(toolUseResult.summary)) || content;
1033
+ const summary = (text.match(/<tool_use_error>([\s\S]*?)<\/tool_use_error>/)?.[1] || text).trim();
1034
+ return summary ? truncateToolResultContent(summary).content : void 0;
1035
+ }
1036
+ function safeJsonStringify(value) {
1037
+ try {
1038
+ return JSON.stringify(value);
1039
+ } catch {
1040
+ return String(value);
1041
+ }
1042
+ }
1043
+ function tryParseJson$1(value) {
1044
+ try {
1045
+ return JSON.parse(value);
1046
+ } catch {
1047
+ return;
1048
+ }
1049
+ }
962
1050
  //#endregion
963
1051
  //#region src/cc-runtime/process-manager.ts
964
1052
  /**
@@ -1416,8 +1504,11 @@ var SessionProcessManager = class {
1416
1504
  parser.on("toolUseStart", (toolName, toolInput) => {
1417
1505
  getCallbacks()?.onToolUseStart?.(toolName, toolInput);
1418
1506
  });
1419
- parser.on("toolResult", (toolUseId) => {
1420
- getCallbacks()?.onToolResult?.(toolUseId);
1507
+ parser.on("toolResult", (toolUseId, result) => {
1508
+ getCallbacks()?.onToolResult?.(toolUseId, result);
1509
+ });
1510
+ parser.on("unknownToolUse", (toolName, toolUseId) => {
1511
+ getCallbacks()?.onUnknownToolUse?.(toolName, toolUseId);
1421
1512
  });
1422
1513
  parser.on("toolProgress", (toolName, elapsedSeconds) => {
1423
1514
  getCallbacks()?.onToolProgress?.(toolName, elapsedSeconds);
@@ -3679,6 +3770,13 @@ function summarizeString(value) {
3679
3770
  * DELETE /api/chat/sessions/:id — 删除会话
3680
3771
  */
3681
3772
  const log$30 = larkLogger("chat/stream");
3773
+ var SessionFileUploadError = class extends Error {
3774
+ constructor(message, statusCode = 400) {
3775
+ super(message);
3776
+ this.statusCode = statusCode;
3777
+ this.name = "SessionFileUploadError";
3778
+ }
3779
+ };
3682
3780
  const chatRunsBySession = /* @__PURE__ */ new Map();
3683
3781
  const DEFAULT_STALE_RUN_IDLE_MS = 1800 * 1e3;
3684
3782
  /** 统一写 SSE 格式数据到响应流 */
@@ -3725,6 +3823,9 @@ function serializeChatRun(run) {
3725
3823
  lastEventAt: run.lastEventAt,
3726
3824
  lastEventType: run.lastEventType,
3727
3825
  lastToolName: run.lastToolName,
3826
+ lastToolError: run.lastToolError,
3827
+ unknownToolCallCount: run.unknownToolCallCount,
3828
+ lastUnknownToolName: run.lastUnknownToolName,
3728
3829
  idleSeconds,
3729
3830
  adapterBusy: run.status === "running" ? run.adapterBusy : false,
3730
3831
  processStatus,
@@ -3771,6 +3872,31 @@ function getRuntimeEventDedupeKey(event) {
3771
3872
  function isRunVisibleToUser(run, params) {
3772
3873
  return run.tenantKey === params.tenantKey && run.openId === params.openId;
3773
3874
  }
3875
+ function getSessionOwner(chatUser) {
3876
+ return {
3877
+ tenantKey: chatUser.tenantKey,
3878
+ openId: chatUser.openId
3879
+ };
3880
+ }
3881
+ function isSessionOwnedBy$1(session, owner) {
3882
+ return session.tenantKey === owner.tenantKey && session.userId === owner.openId;
3883
+ }
3884
+ async function getOwnedSession(messageStore, sessionId, chatUser) {
3885
+ return await messageStore.getSessionForOwner(sessionId, getSessionOwner(chatUser));
3886
+ }
3887
+ function logSessionDecision(params) {
3888
+ log$30.info("[session-audit] chat session access decision", {
3889
+ action: params.action,
3890
+ decision: params.decision,
3891
+ tenantKey: params.chatUser.tenantKey,
3892
+ userId: params.chatUser.userId,
3893
+ openId: params.chatUser.openId,
3894
+ sessionId: params.sessionId,
3895
+ requestId: params.requestId,
3896
+ traceId: params.traceId,
3897
+ reason: params.reason
3898
+ });
3899
+ }
3774
3900
  function addRunClient(req, res, run) {
3775
3901
  run.clients.add(res);
3776
3902
  req.on("close", () => {
@@ -4009,6 +4135,232 @@ function buildRuntimeEventMessageMetadata(event, runtimeConfig) {
4009
4135
  finalStatus: event.finalStatus
4010
4136
  };
4011
4137
  }
4138
+ async function handleSessionFileUpload(req, res, messageStore) {
4139
+ const chatUser = res.locals.chatUser;
4140
+ const sessionId = String(req.params.id);
4141
+ const requestId = getGatewayRequestId(req.headers);
4142
+ const traceId = getGatewayTraceId(req.headers, requestId);
4143
+ if (!await getOwnedSession(messageStore, sessionId, chatUser)) {
4144
+ logSessionDecision({
4145
+ action: "attach",
4146
+ decision: "denied",
4147
+ chatUser,
4148
+ sessionId,
4149
+ requestId,
4150
+ traceId,
4151
+ reason: "session_not_owned"
4152
+ });
4153
+ res.status(404).json({ error: "Session not found" });
4154
+ return;
4155
+ }
4156
+ logSessionDecision({
4157
+ action: "attach",
4158
+ decision: "allowed",
4159
+ chatUser,
4160
+ sessionId,
4161
+ requestId,
4162
+ traceId
4163
+ });
4164
+ if (!req.headers["content-type"]?.includes("multipart/form-data")) {
4165
+ res.status(415).json({ error: "Content-Type must be multipart/form-data" });
4166
+ return;
4167
+ }
4168
+ try {
4169
+ const file = await parseSessionFileUpload(req, sessionId);
4170
+ await messageStore.appendMessage({
4171
+ sessionId,
4172
+ role: "tool",
4173
+ content: JSON.stringify({
4174
+ runtimeEventType: "session-file-attached",
4175
+ file
4176
+ }),
4177
+ channel: "web",
4178
+ metadata: {
4179
+ requestId,
4180
+ traceId,
4181
+ runtimeEventType: "session-file-attached",
4182
+ fileName: file.fileName,
4183
+ relativePath: file.relativePath,
4184
+ mimeType: file.mimeType,
4185
+ size: file.size,
4186
+ attachments: [file]
4187
+ }
4188
+ });
4189
+ res.status(201).json({
4190
+ file,
4191
+ files: [file]
4192
+ });
4193
+ } catch (err) {
4194
+ const statusCode = err instanceof SessionFileUploadError ? err.statusCode : 500;
4195
+ const message = err instanceof Error ? err.message : String(err);
4196
+ log$30.warn("[sessions] session file upload failed", {
4197
+ tenantKey: chatUser.tenantKey,
4198
+ userId: chatUser.userId,
4199
+ openId: chatUser.openId,
4200
+ sessionId,
4201
+ requestId,
4202
+ traceId,
4203
+ statusCode,
4204
+ message
4205
+ });
4206
+ res.status(statusCode).json({ error: message });
4207
+ }
4208
+ }
4209
+ function parseSessionFileUpload(req, sessionId) {
4210
+ return new Promise((resolvePromise, rejectPromise) => {
4211
+ const busboy = Busboy({
4212
+ headers: req.headers,
4213
+ limits: {
4214
+ fileSize: getSessionFileUploadMaxBytes(),
4215
+ files: 1
4216
+ }
4217
+ });
4218
+ let metadata;
4219
+ let uploadPromise;
4220
+ let fileSeen = false;
4221
+ let settled = false;
4222
+ const fail = (error) => {
4223
+ if (settled) return;
4224
+ settled = true;
4225
+ rejectPromise(error);
4226
+ };
4227
+ busboy.on("field", (name, value) => {
4228
+ if (name !== "metadata") return;
4229
+ if (!value.trim()) return;
4230
+ try {
4231
+ const parsed = JSON.parse(value);
4232
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4233
+ metadata = parsed;
4234
+ return;
4235
+ }
4236
+ fail(new SessionFileUploadError("metadata must be a JSON object"));
4237
+ } catch (err) {
4238
+ fail(new SessionFileUploadError(`metadata is not valid JSON: ${err instanceof Error ? err.message : String(err)}`));
4239
+ }
4240
+ });
4241
+ busboy.on("file", (_name, file, info) => {
4242
+ if (fileSeen) {
4243
+ file.resume();
4244
+ fail(new SessionFileUploadError("Only one file is supported per upload"));
4245
+ return;
4246
+ }
4247
+ fileSeen = true;
4248
+ uploadPromise = writeSessionFile(sessionId, {
4249
+ file,
4250
+ originalName: info.filename || "upload.bin",
4251
+ mimeType: info.mimeType || "application/octet-stream",
4252
+ metadata
4253
+ });
4254
+ });
4255
+ busboy.on("filesLimit", () => fail(new SessionFileUploadError("Only one file is supported per upload")));
4256
+ busboy.on("error", fail);
4257
+ busboy.on("finish", () => {
4258
+ if (settled) return;
4259
+ if (!fileSeen || !uploadPromise) {
4260
+ fail(new SessionFileUploadError("file is required"));
4261
+ return;
4262
+ }
4263
+ uploadPromise.then((file) => {
4264
+ if (settled) return;
4265
+ settled = true;
4266
+ resolvePromise(file);
4267
+ }, fail);
4268
+ });
4269
+ req.pipe(busboy);
4270
+ });
4271
+ }
4272
+ function writeSessionFile(sessionId, params) {
4273
+ return new Promise((resolvePromise, rejectPromise) => {
4274
+ const uploadsDir = resolve(resolveAgentCwd(sessionId), "uploads");
4275
+ const fileName = buildSafeUploadFileName(params.originalName);
4276
+ const destination = resolve(uploadsDir, fileName);
4277
+ const relativePath = `uploads/${fileName}`;
4278
+ const maxBytes = getSessionFileUploadMaxBytes();
4279
+ let size = 0;
4280
+ let settled = false;
4281
+ const fail = (error) => {
4282
+ if (settled) return;
4283
+ settled = true;
4284
+ rejectPromise(error);
4285
+ };
4286
+ if (!isPathInside(uploadsDir, destination)) {
4287
+ fail(new SessionFileUploadError("Invalid upload path"));
4288
+ params.file.resume();
4289
+ return;
4290
+ }
4291
+ mkdir(uploadsDir, { recursive: true }).then(() => {
4292
+ const output = createWriteStream(destination, { flags: "wx" });
4293
+ params.file.on("data", (chunk) => {
4294
+ size += chunk.length;
4295
+ if (size > maxBytes) {
4296
+ output.destroy();
4297
+ fail(new SessionFileUploadError(`File exceeds maximum upload size of ${maxBytes} bytes`, 413));
4298
+ }
4299
+ });
4300
+ params.file.on("limit", () => {
4301
+ output.destroy();
4302
+ fail(new SessionFileUploadError(`File exceeds maximum upload size of ${maxBytes} bytes`, 413));
4303
+ });
4304
+ params.file.on("error", fail);
4305
+ output.on("error", fail);
4306
+ output.on("finish", () => {
4307
+ if (settled) return;
4308
+ settled = true;
4309
+ resolvePromise({
4310
+ sessionId,
4311
+ kind: "session-file",
4312
+ fileName,
4313
+ relativePath,
4314
+ uri: `session-file://${relativePath}`,
4315
+ mimeType: params.mimeType,
4316
+ size,
4317
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4318
+ metadata: params.metadata
4319
+ });
4320
+ });
4321
+ params.file.pipe(output);
4322
+ }, fail);
4323
+ });
4324
+ }
4325
+ function getSessionFileUploadMaxBytes() {
4326
+ const raw = process.env.LARKPAL_SESSION_FILE_MAX_UPLOAD_BYTES || process.env.LARKPAL_ARTIFACT_MAX_UPLOAD_BYTES;
4327
+ if (!raw) return 50 * 1024 * 1024;
4328
+ const parsed = Number.parseInt(raw, 10);
4329
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 50 * 1024 * 1024;
4330
+ }
4331
+ function buildSafeUploadFileName(originalName) {
4332
+ const base = basename(originalName).replace(/[\0\r\n\\/]/g, "_");
4333
+ const extension = extname(base).replace(/[^A-Za-z0-9.]/g, "").slice(0, 32);
4334
+ const stem = basename(base, extname(base)).replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+$/, "").slice(0, 80) || "upload";
4335
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${stem}${extension}`;
4336
+ }
4337
+ function isPathInside(parent, child) {
4338
+ const path = relative(parent, child);
4339
+ return path !== "" && !path.startsWith("..") && !isAbsolute(path);
4340
+ }
4341
+ function buildToolResultPayload(result, resultSummary) {
4342
+ if (!result || typeof result !== "object" || Array.isArray(result)) return { result: resultSummary };
4343
+ const record = result;
4344
+ return {
4345
+ rawType: record.rawType,
4346
+ isError: record.isError,
4347
+ content: record.content,
4348
+ resultSummary: record.resultSummary ?? resultSummary,
4349
+ truncated: record.truncated,
4350
+ originalSize: record.originalSize,
4351
+ result: resultSummary
4352
+ };
4353
+ }
4354
+ function readStringField(value, key) {
4355
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4356
+ const field = value[key];
4357
+ return typeof field === "string" ? field : void 0;
4358
+ }
4359
+ function readBooleanField(value, key) {
4360
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4361
+ const field = value[key];
4362
+ return typeof field === "boolean" ? field : void 0;
4363
+ }
4012
4364
  /**
4013
4365
  * 创建 Chat 流式对话 + 历史查询路由
4014
4366
  *
@@ -4025,7 +4377,8 @@ function createChatRouter(config) {
4025
4377
  const router = Router();
4026
4378
  const { processManager, messageStore } = config;
4027
4379
  router.post("/api/chat/stream", chatAuthMiddleware, async (req, res) => {
4028
- const { userId, openId, tenantKey } = res.locals.chatUser;
4380
+ const chatUser = res.locals.chatUser;
4381
+ const { userId, openId, tenantKey } = chatUser;
4029
4382
  const body = req.body;
4030
4383
  if (!tenantKey || !userId || !openId) {
4031
4384
  res.status(401).json({ error: "Missing required chat identity: tenantKey, userId, openId" });
@@ -4034,7 +4387,48 @@ function createChatRouter(config) {
4034
4387
  const requestId = getGatewayRequestId(req.headers);
4035
4388
  const traceId = getGatewayTraceId(req.headers, requestId);
4036
4389
  let sessionId = body.session_id;
4390
+ let streamSession = null;
4037
4391
  const activeRun = sessionId ? getActiveChatRun(sessionId) : void 0;
4392
+ if (sessionId) {
4393
+ streamSession = await messageStore.getSession(sessionId);
4394
+ if (streamSession && !isSessionOwnedBy$1(streamSession, getSessionOwner(chatUser))) {
4395
+ logSessionDecision({
4396
+ action: "stream",
4397
+ decision: "denied",
4398
+ chatUser,
4399
+ sessionId,
4400
+ requestId,
4401
+ traceId,
4402
+ reason: "session_not_owned"
4403
+ });
4404
+ res.status(404).json({ error: "Session not found" });
4405
+ return;
4406
+ }
4407
+ if (!streamSession && activeRun && !isRunVisibleToUser(activeRun, {
4408
+ tenantKey,
4409
+ openId
4410
+ })) {
4411
+ logSessionDecision({
4412
+ action: "stream",
4413
+ decision: "denied",
4414
+ chatUser,
4415
+ sessionId,
4416
+ requestId,
4417
+ traceId,
4418
+ reason: "active_run_not_owned"
4419
+ });
4420
+ res.status(404).json({ error: "Session not found" });
4421
+ return;
4422
+ }
4423
+ if (streamSession || activeRun) logSessionDecision({
4424
+ action: "stream",
4425
+ decision: "allowed",
4426
+ chatUser,
4427
+ sessionId,
4428
+ requestId,
4429
+ traceId
4430
+ });
4431
+ }
4038
4432
  if (activeRun) {
4039
4433
  if (!isRunVisibleToUser(activeRun, {
4040
4434
  tenantKey,
@@ -4057,6 +4451,22 @@ function createChatRouter(config) {
4057
4451
  res.status(400).json({ error: "prompt is required and must be a string" });
4058
4452
  return;
4059
4453
  }
4454
+ if (sessionId && !streamSession) {
4455
+ await messageStore.createSession({
4456
+ id: sessionId,
4457
+ userId: openId,
4458
+ tenantKey,
4459
+ channel: "web"
4460
+ });
4461
+ logSessionDecision({
4462
+ action: "stream",
4463
+ decision: "allowed",
4464
+ chatUser,
4465
+ sessionId,
4466
+ requestId,
4467
+ traceId
4468
+ });
4469
+ }
4060
4470
  const scenarioId = getScenarioId(body.options?.scenarioId, body.options?.metadata);
4061
4471
  const attachments = getRunInputValue(body.attachments, body.options?.attachments, body.options?.metadata?.attachments);
4062
4472
  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 +4487,14 @@ function createChatRouter(config) {
4077
4487
  tenantKey,
4078
4488
  channel: "web"
4079
4489
  })).id;
4490
+ logSessionDecision({
4491
+ action: "create",
4492
+ decision: "allowed",
4493
+ chatUser,
4494
+ sessionId,
4495
+ requestId,
4496
+ traceId
4497
+ });
4080
4498
  log$30.info("[stream] 自动创建新会话", {
4081
4499
  userId: openId,
4082
4500
  sessionId
@@ -4293,8 +4711,12 @@ function createChatRouter(config) {
4293
4711
  if (run.status !== "running") return;
4294
4712
  recordRunEvent(run, "tool-result");
4295
4713
  const resultSummary = summarizeForAudit(result);
4714
+ const toolName = readStringField(result, "toolName") || run.lastToolName;
4715
+ const isError = readBooleanField(result, "isError");
4716
+ if (isError) run.lastToolError = resultSummary;
4296
4717
  if (runtimeConfig.requestContext) logChatAudit(buildAuditEvent(runtimeConfig.requestContext, "tool_call_finished", {
4297
4718
  toolCallId: toolUseId,
4719
+ toolName,
4298
4720
  resultSummary,
4299
4721
  metadata: buildToolResultAuditMetadata(result)
4300
4722
  }));
@@ -4303,7 +4725,8 @@ function createChatRouter(config) {
4303
4725
  role: "tool",
4304
4726
  content: JSON.stringify({
4305
4727
  toolUseId,
4306
- result: resultSummary
4728
+ toolName,
4729
+ ...buildToolResultPayload(result, resultSummary)
4307
4730
  }),
4308
4731
  channel: "web",
4309
4732
  metadata: {
@@ -4313,12 +4736,33 @@ function createChatRouter(config) {
4313
4736
  scenarioId,
4314
4737
  runtimeEventType: "tool-result",
4315
4738
  toolCallId: toolUseId,
4316
- toolResult: resultSummary
4739
+ toolName,
4740
+ toolResult: resultSummary,
4741
+ isError
4317
4742
  }
4318
4743
  }, "[stream] 保存 tool result 消息失败");
4319
4744
  broadcastRunEvent(run, "tool-result", {
4320
4745
  toolUseId,
4321
- result: resultSummary
4746
+ toolName,
4747
+ ...buildToolResultPayload(result, resultSummary)
4748
+ });
4749
+ },
4750
+ onUnknownToolUse: (toolName, toolUseId) => {
4751
+ if (run.status !== "running") return;
4752
+ run.unknownToolCallCount = (run.unknownToolCallCount ?? 0) + 1;
4753
+ run.lastUnknownToolName = toolName;
4754
+ run.lastToolError = {
4755
+ isError: true,
4756
+ reasonCode: "UNKNOWN_TOOL_CALL",
4757
+ toolName,
4758
+ toolCallId: toolUseId
4759
+ };
4760
+ recordRunEvent(run, "unknown-tool-use", { toolName });
4761
+ broadcastRunEvent(run, "runtime-event", {
4762
+ type: "unknown-tool-use",
4763
+ toolName,
4764
+ toolUseId,
4765
+ unknownToolCallCount: run.unknownToolCallCount
4322
4766
  });
4323
4767
  },
4324
4768
  onToolProgress: (toolName, elapsedSeconds) => {
@@ -4559,6 +5003,29 @@ function createChatRouter(config) {
4559
5003
  router.post("/api/chat/sessions/:id/cancel-active-run", chatAuthMiddleware, async (req, res) => {
4560
5004
  const chatUser = res.locals.chatUser;
4561
5005
  const sessionId = String(req.params.id);
5006
+ const requestId = getGatewayRequestId(req.headers);
5007
+ const traceId = getGatewayTraceId(req.headers, requestId);
5008
+ if (!await getOwnedSession(messageStore, sessionId, chatUser)) {
5009
+ logSessionDecision({
5010
+ action: "cancel",
5011
+ decision: "denied",
5012
+ chatUser,
5013
+ sessionId,
5014
+ requestId,
5015
+ traceId,
5016
+ reason: "session_not_owned"
5017
+ });
5018
+ res.status(404).json({ error: "Session not found" });
5019
+ return;
5020
+ }
5021
+ logSessionDecision({
5022
+ action: "cancel",
5023
+ decision: "allowed",
5024
+ chatUser,
5025
+ sessionId,
5026
+ requestId,
5027
+ traceId
5028
+ });
4562
5029
  const run = chatRunsBySession.get(sessionId);
4563
5030
  if (!run) {
4564
5031
  res.json({
@@ -4596,6 +5063,9 @@ function createChatRouter(config) {
4596
5063
  });
4597
5064
  }
4598
5065
  });
5066
+ router.post("/api/chat/sessions/:id/files", chatAuthMiddleware, async (req, res) => {
5067
+ await handleSessionFileUpload(req, res, messageStore);
5068
+ });
4599
5069
  router.get("/api/chat/history", chatAuthMiddleware, async (req, res) => {
4600
5070
  const chatUser = res.locals.chatUser;
4601
5071
  const sessionId = req.query.session_id;
@@ -4605,6 +5075,8 @@ function createChatRouter(config) {
4605
5075
  }
4606
5076
  const cursor = req.query.cursor;
4607
5077
  const limit = parseInt(req.query.limit, 10) || 50;
5078
+ const requestId = getGatewayRequestId(req.headers);
5079
+ const traceId = getGatewayTraceId(req.headers, requestId);
4608
5080
  log$30.info("[history] 查询会话历史", {
4609
5081
  userId: chatUser.userId,
4610
5082
  sessionId,
@@ -4612,6 +5084,27 @@ function createChatRouter(config) {
4612
5084
  limit
4613
5085
  });
4614
5086
  try {
5087
+ if (!await getOwnedSession(messageStore, sessionId, chatUser)) {
5088
+ logSessionDecision({
5089
+ action: "history",
5090
+ decision: "denied",
5091
+ chatUser,
5092
+ sessionId,
5093
+ requestId,
5094
+ traceId,
5095
+ reason: "session_not_owned"
5096
+ });
5097
+ res.status(404).json({ error: "Session not found" });
5098
+ return;
5099
+ }
5100
+ logSessionDecision({
5101
+ action: "history",
5102
+ decision: "allowed",
5103
+ chatUser,
5104
+ sessionId,
5105
+ requestId,
5106
+ traceId
5107
+ });
4615
5108
  const result = await messageStore.getMessages(sessionId, {
4616
5109
  cursor,
4617
5110
  limit
@@ -4647,6 +5140,29 @@ function createChatRouter(config) {
4647
5140
  res.status(400).json({ error: "session_id is required" });
4648
5141
  return;
4649
5142
  }
5143
+ const requestId = getGatewayRequestId(req.headers);
5144
+ const traceId = getGatewayTraceId(req.headers, requestId);
5145
+ if (!await getOwnedSession(messageStore, sessionId, chatUser)) {
5146
+ logSessionDecision({
5147
+ action: "runs",
5148
+ decision: "denied",
5149
+ chatUser,
5150
+ sessionId,
5151
+ requestId,
5152
+ traceId,
5153
+ reason: "session_not_owned"
5154
+ });
5155
+ res.status(404).json({ error: "Session not found" });
5156
+ return;
5157
+ }
5158
+ logSessionDecision({
5159
+ action: "runs",
5160
+ decision: "allowed",
5161
+ chatUser,
5162
+ sessionId,
5163
+ requestId,
5164
+ traceId
5165
+ });
4650
5166
  const run = chatRunsBySession.get(sessionId);
4651
5167
  if (!run || !isRunVisibleToUser(run, {
4652
5168
  tenantKey: chatUser.tenantKey,
@@ -4673,7 +5189,7 @@ function createChatRouter(config) {
4673
5189
  const chatUser = res.locals.chatUser;
4674
5190
  log$30.info("[sessions] 列出用户会话", { userId: chatUser.userId });
4675
5191
  try {
4676
- const sessions = await messageStore.listSessions(chatUser.openId);
5192
+ const sessions = await messageStore.listSessions(chatUser.openId, chatUser.tenantKey);
4677
5193
  res.json({ sessions });
4678
5194
  } catch (err) {
4679
5195
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -4717,7 +5233,29 @@ function createChatRouter(config) {
4717
5233
  sessionId
4718
5234
  });
4719
5235
  try {
4720
- await messageStore.deleteSession(sessionId);
5236
+ const requestId = getGatewayRequestId(req.headers);
5237
+ const traceId = getGatewayTraceId(req.headers, requestId);
5238
+ if (!await messageStore.deleteSessionForOwner(sessionId, getSessionOwner(chatUser))) {
5239
+ logSessionDecision({
5240
+ action: "delete",
5241
+ decision: "denied",
5242
+ chatUser,
5243
+ sessionId,
5244
+ requestId,
5245
+ traceId,
5246
+ reason: "session_not_owned"
5247
+ });
5248
+ res.status(404).json({ error: "Session not found" });
5249
+ return;
5250
+ }
5251
+ logSessionDecision({
5252
+ action: "delete",
5253
+ decision: "allowed",
5254
+ chatUser,
5255
+ sessionId,
5256
+ requestId,
5257
+ traceId
5258
+ });
4721
5259
  chatRunsBySession.delete(sessionId);
4722
5260
  res.json({ success: true });
4723
5261
  } catch (err) {
@@ -7935,6 +8473,11 @@ var SessionMessageStore = class {
7935
8473
  * 创建新会话
7936
8474
  */
7937
8475
  async createSession(session) {
8476
+ const existing = await this.getSession(session.id);
8477
+ if (existing) {
8478
+ if (existing.userId === session.userId && existing.tenantKey === session.tenantKey) return existing;
8479
+ throw new Error(`Session ${session.id} already exists for a different owner`);
8480
+ }
7938
8481
  const now = Date.now();
7939
8482
  const fullSession = {
7940
8483
  ...session,
@@ -7946,9 +8489,9 @@ var SessionMessageStore = class {
7946
8489
  sessionId: fullSession.id,
7947
8490
  userId: fullSession.userId
7948
8491
  });
7949
- const sessions = await this.readUserSessions(fullSession.userId);
8492
+ const sessions = await this.readUserSessions(fullSession.userId, fullSession.tenantKey);
7950
8493
  sessions.push(fullSession);
7951
- await this.writeUserSessions(fullSession.userId, sessions);
8494
+ await this.writeUserSessions(fullSession.userId, sessions, fullSession.tenantKey);
7952
8495
  return fullSession;
7953
8496
  }
7954
8497
  /**
@@ -7975,9 +8518,21 @@ var SessionMessageStore = class {
7975
8518
  /**
7976
8519
  * 列出指定用户的所有会话
7977
8520
  */
7978
- async listSessions(userId) {
7979
- log$24.debug("列出用户会话", { userId });
7980
- return await this.readUserSessions(userId);
8521
+ async listSessions(userId, tenantKey) {
8522
+ log$24.debug("列出用户会话", {
8523
+ userId,
8524
+ tenantKey
8525
+ });
8526
+ const sessions = await this.readUserSessions(userId, tenantKey);
8527
+ if (!tenantKey) return sessions;
8528
+ const ownedLegacySessions = (await this.readUserSessions(userId)).filter((session) => session.userId === userId && session.tenantKey === tenantKey);
8529
+ const seen = new Set(sessions.map((session) => session.id));
8530
+ return [...sessions, ...ownedLegacySessions.filter((session) => !seen.has(session.id))];
8531
+ }
8532
+ async getSessionForOwner(sessionId, owner) {
8533
+ const session = await this.getSession(sessionId);
8534
+ if (!session) return null;
8535
+ return isSessionOwnedBy(session, owner) ? session : null;
7981
8536
  }
7982
8537
  /**
7983
8538
  * 删除指定会话(同时删除消息文件)
@@ -8007,6 +8562,12 @@ var SessionMessageStore = class {
8007
8562
  throw err;
8008
8563
  }
8009
8564
  }
8565
+ async deleteSessionForOwner(sessionId, owner) {
8566
+ const session = await this.getSession(sessionId);
8567
+ if (!session || !isSessionOwnedBy(session, owner)) return false;
8568
+ await this.deleteSession(sessionId);
8569
+ return true;
8570
+ }
8010
8571
  /**
8011
8572
  * 追加消息到会话
8012
8573
  * 同时更新会话的 updatedAt 和 messageCount
@@ -8096,21 +8657,21 @@ var SessionMessageStore = class {
8096
8657
  /**
8097
8658
  * 获取用户会话索引文件路径
8098
8659
  */
8099
- getUserSessionFilePath(userId) {
8100
- return join(this.sessionsDir, `${userId}.json`);
8660
+ getUserSessionFilePath(userId, tenantKey) {
8661
+ return join(this.sessionsDir, `${buildOwnerKey(userId, tenantKey)}.json`);
8101
8662
  }
8102
8663
  /**
8103
8664
  * 读取用户的会话列表
8104
8665
  */
8105
- async readUserSessions(userId) {
8106
- const filePath = this.getUserSessionFilePath(userId);
8666
+ async readUserSessions(userId, tenantKey) {
8667
+ const filePath = this.getUserSessionFilePath(userId, tenantKey);
8107
8668
  return await this.readJsonFile(filePath) || [];
8108
8669
  }
8109
8670
  /**
8110
8671
  * 写入用户的会话列表
8111
8672
  */
8112
- async writeUserSessions(userId, sessions) {
8113
- const filePath = this.getUserSessionFilePath(userId);
8673
+ async writeUserSessions(userId, sessions, tenantKey) {
8674
+ const filePath = this.getUserSessionFilePath(userId, tenantKey);
8114
8675
  await this.writeJsonFile(filePath, sessions);
8115
8676
  }
8116
8677
  /**
@@ -8210,6 +8771,12 @@ var SessionMessageStore = class {
8210
8771
  } catch {}
8211
8772
  }
8212
8773
  };
8774
+ function isSessionOwnedBy(session, owner) {
8775
+ return session.userId === owner.openId && session.tenantKey === owner.tenantKey;
8776
+ }
8777
+ function buildOwnerKey(userId, tenantKey) {
8778
+ return (tenantKey ? `${tenantKey}__${userId}` : userId).replace(/[^A-Za-z0-9_.:-]/g, "_");
8779
+ }
8213
8780
  //#endregion
8214
8781
  //#region src/messaging/inbound/user-name-cache-store.ts
8215
8782
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",