@vibe-lark/larkpal 0.1.77 → 0.1.79

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 +212 -128
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -251,88 +251,98 @@ function getWorkspaceChatHistoryRoot(workspaceRoot = resolveWorkspaceRoot()) {
251
251
  * 以及确保会话工作目录结构完整。
252
252
  */
253
253
  const logger$10 = larkLogger("config/defaults");
254
- const DEFAULT_SETTINGS = {
255
- permissions: {
256
- allow: [
257
- "Bash(*)",
258
- "Read(*)",
259
- "Write(*)",
260
- "Edit(*)",
261
- "MultiEdit(*)",
262
- "Glob(*)",
263
- "Grep(*)",
264
- "LS(*)",
265
- "TodoWrite(*)",
266
- "WebFetch(*)",
267
- "WebSearch(*)"
268
- ],
269
- deny: [
270
- "Write(//.claude/CLAUDE.md)",
271
- "Edit(//.claude/CLAUDE.md)",
272
- "Write(//.claude/settings.json)",
273
- "Edit(//.claude/settings.json)",
274
- "Read(//.lark-cli/**)",
275
- "Read(//.config/lark/**)",
276
- "Read(//.larkpal/credentials.json)",
277
- "Read(//.env)",
278
- "Read(//.env.*)",
279
- "Bash(cat ~/.lark-cli:*)",
280
- "Bash(cat ~/.config/lark:*)",
281
- "Bash(cat ~/.larkpal/credentials:*)",
282
- "Bash(head ~/.lark-cli:*)",
283
- "Bash(head ~/.config/lark:*)",
284
- "Bash(tail ~/.lark-cli:*)",
285
- "Bash(tail ~/.config/lark:*)",
286
- "Bash(less ~/.lark-cli:*)",
287
- "Bash(less ~/.config/lark:*)",
288
- "Bash(more ~/.lark-cli:*)",
289
- "Bash(more ~/.config/lark:*)",
290
- "Bash(env:*)",
291
- "Bash(printenv:*)",
292
- "Bash(export -p:*)",
293
- "Bash(echo $LARK_APP_SECRET:*)",
294
- "Bash(echo $ANTHROPIC_API_KEY:*)"
295
- ]
296
- },
297
- hooks: {
298
- SessionStart: [{ hooks: [{
299
- type: "http",
300
- url: "http://localhost:3000/hooks/session-start",
301
- timeout: 5
302
- }] }],
303
- Stop: [{ hooks: [{
304
- type: "http",
305
- url: "http://localhost:3000/hooks/stop",
306
- timeout: 5
307
- }] }],
308
- PreToolUse: [{
309
- matcher: "Read",
310
- hooks: [{
254
+ function createDefaultSettings() {
255
+ const hookBaseUrl = getClaudeHookBaseUrl();
256
+ return {
257
+ permissions: {
258
+ allow: [
259
+ "Bash(*)",
260
+ "Read(*)",
261
+ "Write(*)",
262
+ "Edit(*)",
263
+ "MultiEdit(*)",
264
+ "Glob(*)",
265
+ "Grep(*)",
266
+ "LS(*)",
267
+ "TodoWrite(*)",
268
+ "WebFetch(*)",
269
+ "WebSearch(*)"
270
+ ],
271
+ deny: [
272
+ "Write(//.claude/CLAUDE.md)",
273
+ "Edit(//.claude/CLAUDE.md)",
274
+ "Write(//.claude/settings.json)",
275
+ "Edit(//.claude/settings.json)",
276
+ "Read(//.lark-cli/**)",
277
+ "Read(//.config/lark/**)",
278
+ "Read(//.larkpal/credentials.json)",
279
+ "Read(//.env)",
280
+ "Read(//.env.*)",
281
+ "Bash(cat ~/.lark-cli:*)",
282
+ "Bash(cat ~/.config/lark:*)",
283
+ "Bash(cat ~/.larkpal/credentials:*)",
284
+ "Bash(head ~/.lark-cli:*)",
285
+ "Bash(head ~/.config/lark:*)",
286
+ "Bash(tail ~/.lark-cli:*)",
287
+ "Bash(tail ~/.config/lark:*)",
288
+ "Bash(less ~/.lark-cli:*)",
289
+ "Bash(less ~/.config/lark:*)",
290
+ "Bash(more ~/.lark-cli:*)",
291
+ "Bash(more ~/.config/lark:*)",
292
+ "Bash(env:*)",
293
+ "Bash(printenv:*)",
294
+ "Bash(export -p:*)",
295
+ "Bash(echo $LARK_APP_SECRET:*)",
296
+ "Bash(echo $ANTHROPIC_API_KEY:*)"
297
+ ]
298
+ },
299
+ hooks: {
300
+ SessionStart: [{ hooks: [{
311
301
  type: "http",
312
- url: "http://localhost:3000/hooks/pre-tool-use",
302
+ url: `${hookBaseUrl}/hooks/session-start`,
313
303
  timeout: 5
314
- }]
315
- }],
316
- PostToolUse: [{
317
- matcher: "Read",
318
- hooks: [{
304
+ }] }],
305
+ Stop: [{ hooks: [{
319
306
  type: "http",
320
- url: "http://localhost:3000/hooks/post-tool-use",
307
+ url: `${hookBaseUrl}/hooks/stop`,
321
308
  timeout: 5
322
- }]
323
- }],
324
- SessionEnd: [{ hooks: [{
325
- type: "http",
326
- url: "http://localhost:3000/hooks/session-end",
327
- timeout: 5
328
- }] }],
329
- Notification: [{ hooks: [{
330
- type: "http",
331
- url: "http://localhost:3000/hooks/notification",
332
- timeout: 5
333
- }] }]
334
- }
335
- };
309
+ }] }],
310
+ PreToolUse: [{
311
+ matcher: "Read",
312
+ hooks: [{
313
+ type: "http",
314
+ url: `${hookBaseUrl}/hooks/pre-tool-use`,
315
+ timeout: 5
316
+ }]
317
+ }],
318
+ PostToolUse: [{
319
+ matcher: "Read",
320
+ hooks: [{
321
+ type: "http",
322
+ url: `${hookBaseUrl}/hooks/post-tool-use`,
323
+ timeout: 5
324
+ }]
325
+ }],
326
+ SessionEnd: [{ hooks: [{
327
+ type: "http",
328
+ url: `${hookBaseUrl}/hooks/session-end`,
329
+ timeout: 5
330
+ }] }],
331
+ Notification: [{ hooks: [{
332
+ type: "http",
333
+ url: `${hookBaseUrl}/hooks/notification`,
334
+ timeout: 5
335
+ }] }]
336
+ }
337
+ };
338
+ }
339
+ function getClaudeHookBaseUrl() {
340
+ const explicit = process.env.LARKPAL_CC_HOOK_BASE_URL?.trim();
341
+ if (explicit) return explicit.replace(/\/+$/, "");
342
+ const port = process.env.LARKPAL_GATEWAY_PORT || "3000";
343
+ const rawHost = process.env.LARKPAL_GATEWAY_HOST || "localhost";
344
+ return `http://${rawHost === "0.0.0.0" || rawHost === "::" ? "127.0.0.1" : rawHost}:${port}`;
345
+ }
336
346
  const DEFAULT_CLAUDE_MD = `# LarkPal
337
347
 
338
348
  你是一个运行在飞书中的 AI 协作伙伴。这是你的初始状态。
@@ -430,7 +440,7 @@ async function ensureDefaults() {
430
440
  const claudeMdPath = join(claudeDir, "CLAUDE.md");
431
441
  await mkdir(claudeDir, { recursive: true });
432
442
  logger$10.info("确保 ~/.claude 目录存在", { path: claudeDir });
433
- await writeFile(settingsPath, JSON.stringify(DEFAULT_SETTINGS, null, 2), "utf-8");
443
+ await writeFile(settingsPath, JSON.stringify(createDefaultSettings(), null, 2), "utf-8");
434
444
  logger$10.info("settings.json 已同步(强制覆盖)", { path: settingsPath });
435
445
  if (await fileExists(claudeMdPath)) logger$10.info("CLAUDE.md 已存在,跳过生成", { path: claudeMdPath });
436
446
  else {
@@ -878,7 +888,7 @@ var CCStreamParser = class extends EventEmitter {
878
888
  toolUseId: block?.id
879
889
  });
880
890
  if (block?.type === "tool_use" && block.name) {
881
- this.recordToolUse(block.name, block.id);
891
+ if (!this.recordToolUse(block.name, block.id, block.input)) break;
882
892
  log$37.info("工具调用开始", {
883
893
  toolName: block.name,
884
894
  displayName: getToolDisplayName(block.name),
@@ -926,7 +936,7 @@ var CCStreamParser = class extends EventEmitter {
926
936
  });
927
937
  if (Array.isArray(content)) {
928
938
  for (const block of content) if (block.type === "tool_use" && block.name) {
929
- this.recordToolUse(block.name, block.id);
939
+ if (!this.recordToolUse(block.name, block.id, block.input)) continue;
930
940
  log$37.info("从 assistant 消息提取工具调用", {
931
941
  toolName: block.name,
932
942
  displayName: getToolDisplayName(block.name),
@@ -960,15 +970,19 @@ var CCStreamParser = class extends EventEmitter {
960
970
  if (msg.subtype !== "init" || !Array.isArray(msg.tools)) return;
961
971
  this.declaredTools = new Set(msg.tools);
962
972
  }
963
- recordToolUse(toolName, toolUseId) {
973
+ recordToolUse(toolName, toolUseId, toolInput) {
964
974
  if (toolUseId) this.toolNamesById.set(toolUseId, toolName);
965
975
  if (this.declaredTools && !this.declaredTools.has(toolName)) {
966
- log$37.warn("模型请求了未声明工具", {
976
+ const availableTools = [...this.declaredTools];
977
+ log$37.warn("模型请求了未声明工具,已抑制正常 toolUseStart", {
967
978
  toolName,
968
- toolUseId
979
+ toolUseId,
980
+ availableTools
969
981
  });
970
- this.emit("unknownToolUse", toolName, toolUseId);
982
+ this.emit("unknownToolUse", toolName, toolUseId, toolInput, availableTools);
983
+ return false;
971
984
  }
985
+ return true;
972
986
  }
973
987
  buildToolResultSummary(item, toolUseResult) {
974
988
  const serializedContent = serializeToolResultContent(item.content);
@@ -1133,30 +1147,64 @@ async function sanitizeTranscriptEntry(entry, artifactRoot) {
1133
1147
  let artifacts = 0;
1134
1148
  const message = asObject$1(entry.message);
1135
1149
  const content = message ? message.content : void 0;
1136
- if (!Array.isArray(content)) return {
1150
+ const toolUseResult = asObject$1(entry.toolUseResult);
1151
+ let firstArtifactUri;
1152
+ if (Array.isArray(content)) for (let i = 0; i < content.length; i++) {
1153
+ const block = asObject$1(content[i]);
1154
+ if (!block) continue;
1155
+ if (block.type === "tool_result") {
1156
+ const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : void 0;
1157
+ const replacement = await sanitizeToolResultContent(block.content, artifactRoot, toolUseId);
1158
+ if (!replacement) continue;
1159
+ block.content = replacement.content;
1160
+ sanitizedImageBlocks += replacement.imageBlocks;
1161
+ artifacts += replacement.artifacts;
1162
+ firstArtifactUri ??= replacement.metadata[0]?.artifactUri;
1163
+ continue;
1164
+ }
1165
+ if (isBase64ImageBlock(block)) {
1166
+ const artifact = await writeImageArtifact(artifactRoot, block, void 0, i);
1167
+ content[i] = {
1168
+ type: "text",
1169
+ text: buildReplacementText([artifact])
1170
+ };
1171
+ sanitizedImageBlocks++;
1172
+ artifacts++;
1173
+ firstArtifactUri ??= artifact.artifactUri;
1174
+ }
1175
+ }
1176
+ const toolUseResultReplacement = await sanitizeToolUseResult(toolUseResult, artifactRoot, firstArtifactUri);
1177
+ if (toolUseResultReplacement) {
1178
+ sanitizedImageBlocks += toolUseResultReplacement.sanitizedImageBlocks;
1179
+ artifacts += toolUseResultReplacement.artifacts;
1180
+ }
1181
+ return {
1137
1182
  sanitizedImageBlocks,
1138
1183
  artifacts
1139
1184
  };
1140
- for (const block of content) {
1141
- const toolResult = asObject$1(block);
1142
- if (!toolResult || toolResult.type !== "tool_result") continue;
1143
- const toolUseId = typeof toolResult.tool_use_id === "string" ? toolResult.tool_use_id : void 0;
1144
- const replacement = await sanitizeToolResultContent(toolResult.content, artifactRoot, toolUseId);
1145
- if (!replacement) continue;
1146
- toolResult.content = replacement.content;
1147
- sanitizedImageBlocks += replacement.imageBlocks;
1148
- artifacts += replacement.artifacts;
1149
- const toolUseResult = asObject$1(entry.toolUseResult);
1150
- if (toolUseResult?.type === "image") {
1151
- const file = asObject$1(toolUseResult.file);
1152
- if (file && typeof file.base64 === "string") {
1153
- file.artifactUri = replacement.metadata[0]?.artifactUri;
1154
- file.base64 = "[redacted by LarkPal: image artifact externalized]";
1155
- }
1156
- }
1185
+ }
1186
+ async function sanitizeToolUseResult(toolUseResult, artifactRoot, existingArtifactUri) {
1187
+ if (toolUseResult?.type !== "image") return null;
1188
+ const file = asObject$1(toolUseResult.file);
1189
+ if (!file || typeof file.base64 !== "string") return null;
1190
+ let artifactUri = existingArtifactUri;
1191
+ let artifacts = 0;
1192
+ if (!artifactUri) {
1193
+ artifactUri = (await writeImageArtifact(artifactRoot, {
1194
+ type: "image",
1195
+ source: {
1196
+ type: "base64",
1197
+ media_type: typeof file.type === "string" ? file.type : void 0,
1198
+ data: file.base64
1199
+ },
1200
+ filePath: typeof file.filePath === "string" ? file.filePath : void 0
1201
+ }, void 0, 0)).artifactUri;
1202
+ artifacts = 1;
1157
1203
  }
1204
+ file.artifactUri = artifactUri;
1205
+ file.base64 = "[redacted by LarkPal: image artifact externalized]";
1158
1206
  return {
1159
- sanitizedImageBlocks,
1207
+ sanitizedImageBlocks: 1,
1160
1208
  artifacts
1161
1209
  };
1162
1210
  }
@@ -1235,7 +1283,7 @@ function formatDimensions(block) {
1235
1283
  return `${width}x${height}`;
1236
1284
  }
1237
1285
  function mightContainImagePayload(line) {
1238
- return line.includes("\"tool_result\"") && line.includes("\"type\":\"image\"") && line.includes("\"base64\"");
1286
+ return line.includes("\"base64\"") && (line.includes("\"image\"") || line.includes("\"source\""));
1239
1287
  }
1240
1288
  function asObject$1(value) {
1241
1289
  return typeof value === "object" && value != null ? value : void 0;
@@ -7284,6 +7332,7 @@ function asObject(value) {
7284
7332
  * 所有 hook 当前只做日志记录,不做复杂业务处理。
7285
7333
  */
7286
7334
  const logger$5 = larkLogger("gateway/hooks");
7335
+ const DELAYED_SANITIZER_RETRY_MS = 1e3;
7287
7336
  /**
7288
7337
  * 创建 HTTP Hooks 路由
7289
7338
  */
@@ -7336,7 +7385,7 @@ function createHooksRouter() {
7336
7385
  });
7337
7386
  router.post("/post-tool-use", async (req, res) => {
7338
7387
  const body = req.body;
7339
- const transcriptPath = typeof body.transcript_path === "string" ? body.transcript_path : typeof body.transcriptPath === "string" ? body.transcriptPath : void 0;
7388
+ const transcriptPath = getTranscriptPath(body);
7340
7389
  logger$5.info("hook received: post-tool-use", {
7341
7390
  method: req.method,
7342
7391
  url: req.originalUrl,
@@ -7344,36 +7393,28 @@ function createHooksRouter() {
7344
7393
  toolName: body.tool_name,
7345
7394
  transcriptPath
7346
7395
  });
7347
- if (!transcriptPath || !isTranscriptSanitizerEnabled()) {
7348
- res.json({ continue: true });
7349
- return;
7350
- }
7351
- try {
7352
- const result = await sanitizeClaudeTranscriptFile(transcriptPath);
7353
- res.json({
7354
- continue: true,
7355
- hookSpecificOutput: result.changed ? {
7356
- hookEventName: "PostToolUse",
7357
- additionalContext: `LarkPal externalized ${result.sanitizedImageBlocks} image tool result(s) from the persisted Claude transcript to reduce future replay payload.`
7358
- } : void 0
7359
- });
7360
- } catch (err) {
7361
- logger$5.warn("post-tool-use transcript sanitizer failed", {
7362
- transcriptPath,
7363
- error: err instanceof Error ? err.message : String(err)
7364
- });
7365
- res.json({ continue: true });
7366
- }
7396
+ const result = await sanitizeTranscriptFromHook("PostToolUse", transcriptPath);
7397
+ if (result?.changed === false) scheduleDelayedTranscriptSanitizer("PostToolUse", transcriptPath);
7398
+ res.json({
7399
+ continue: true,
7400
+ hookSpecificOutput: result?.changed ? {
7401
+ hookEventName: "PostToolUse",
7402
+ additionalContext: `LarkPal externalized ${result.sanitizedImageBlocks} image tool result(s) from the persisted Claude transcript to reduce future replay payload.`
7403
+ } : void 0
7404
+ });
7367
7405
  });
7368
- router.post("/session-end", (req, res) => {
7406
+ router.post("/session-end", async (req, res) => {
7369
7407
  const body = req.body;
7408
+ const transcriptPath = getTranscriptPath(body);
7370
7409
  logger$5.info("hook received: session-end", {
7371
7410
  method: req.method,
7372
7411
  url: req.originalUrl,
7373
7412
  sessionId: body.session_id,
7413
+ transcriptPath,
7374
7414
  body
7375
7415
  });
7376
- resetImageReadGuard(typeof body.transcript_path === "string" ? body.transcript_path : void 0);
7416
+ resetImageReadGuard(transcriptPath);
7417
+ if ((await sanitizeTranscriptFromHook("SessionEnd", transcriptPath))?.changed === false) scheduleDelayedTranscriptSanitizer("SessionEnd", transcriptPath);
7377
7418
  res.json({ received: true });
7378
7419
  });
7379
7420
  router.post("/notification", (req, res) => {
@@ -7387,6 +7428,47 @@ function createHooksRouter() {
7387
7428
  });
7388
7429
  return router;
7389
7430
  }
7431
+ function getTranscriptPath(body) {
7432
+ return typeof body.transcript_path === "string" ? body.transcript_path : typeof body.transcriptPath === "string" ? body.transcriptPath : void 0;
7433
+ }
7434
+ async function sanitizeTranscriptFromHook(hookEventName, transcriptPath) {
7435
+ if (!transcriptPath) {
7436
+ logger$5.info("transcript sanitizer skipped: no transcript path", { hookEventName });
7437
+ return;
7438
+ }
7439
+ if (!isTranscriptSanitizerEnabled()) {
7440
+ logger$5.info("transcript sanitizer skipped: disabled", {
7441
+ hookEventName,
7442
+ transcriptPath
7443
+ });
7444
+ return;
7445
+ }
7446
+ try {
7447
+ const result = await sanitizeClaudeTranscriptFile(transcriptPath);
7448
+ logger$5.info("transcript sanitizer completed", {
7449
+ hookEventName,
7450
+ transcriptPath,
7451
+ changed: result.changed,
7452
+ lines: result.lines,
7453
+ sanitizedImageBlocks: result.sanitizedImageBlocks,
7454
+ artifacts: result.artifacts
7455
+ });
7456
+ return result;
7457
+ } catch (err) {
7458
+ logger$5.warn("transcript sanitizer failed", {
7459
+ hookEventName,
7460
+ transcriptPath,
7461
+ error: err instanceof Error ? err.message : String(err)
7462
+ });
7463
+ return;
7464
+ }
7465
+ }
7466
+ function scheduleDelayedTranscriptSanitizer(hookEventName, transcriptPath) {
7467
+ if (!transcriptPath || !isTranscriptSanitizerEnabled()) return;
7468
+ setTimeout(() => {
7469
+ sanitizeTranscriptFromHook(`${hookEventName}:delayed`, transcriptPath);
7470
+ }, DELAYED_SANITIZER_RETRY_MS).unref();
7471
+ }
7390
7472
  //#endregion
7391
7473
  //#region src/memory/store.ts
7392
7474
  const log$27 = larkLogger("memory/store");
@@ -20091,6 +20173,8 @@ async function main() {
20091
20173
  logger.info("LarkPal 启动中...");
20092
20174
  if (process.env.LARKPAL_HEADLESS === "1" || process.env.LARKPAL_HEADLESS === "true") {
20093
20175
  logger.info("Headless 模式启动 — 跳过飞书凭证/preflight/WebSocket");
20176
+ await ensureDefaults();
20177
+ logger.info("Headless 默认配置检查完成");
20094
20178
  await startHeadless();
20095
20179
  return;
20096
20180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",