larkin 0.4.5 → 0.4.7

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.
@@ -61,7 +61,7 @@ function hasCanonicalUnprotectedCommandPath(argv) {
61
61
  return false;
62
62
  if (service !== "im")
63
63
  return service !== "api" && service !== "larkin-draft";
64
- if (command === "+messages-send" || command === "+messages-reply" || command === "api") {
64
+ if (command === "+messages-send" || command === "+messages-reply" || command === "+messages-urgent-app" || command === "api") {
65
65
  return false;
66
66
  }
67
67
  if (command.startsWith("+"))
@@ -200,6 +200,14 @@ export function classifyLarkCliCommand(argv) {
200
200
  return uniqueProtectedOperation(protectedPaths, "reply")
201
201
  ? (parsed.commandArgv.includes("--dry-run") ? { kind: "passthrough" } : { kind: "guarded", operation: "reply" })
202
202
  : noncanonicalProtectedDecision();
203
+ if (exactPath(parsed.commandArgv, ["im", "+messages-urgent-app"])) {
204
+ return { kind: "denied", reason: "不要使用合成入口 +messages-urgent-app;请使用官方 im messages urgent_app" };
205
+ }
206
+ if (exactPath(parsed.commandArgv, ["im", "messages", "urgent_app"])) {
207
+ return uniqueProtectedOperation(protectedPaths, "raw-urgent_app")
208
+ ? (parsed.commandArgv.includes("--dry-run") || parsed.help ? { kind: "passthrough" } : { kind: "guarded", operation: "urgent-app" })
209
+ : noncanonicalProtectedDecision();
210
+ }
203
211
  if (exactPath(parsed.commandArgv, ["im", "messages", "patch"]) || exactPath(parsed.commandArgv, ["im", "messages", "update"])) {
204
212
  const expected = parsed.commandArgv[2] === "patch" ? "card-patch" : "card-update";
205
213
  return uniqueProtectedOperation(protectedPaths, expected)
@@ -212,7 +220,7 @@ export function classifyLarkCliCommand(argv) {
212
220
  ? { kind: "denied", reason: "该原始 IM 写入口会旁路 target freshness;请使用 +messages-send/+messages-reply" }
213
221
  : noncanonicalProtectedDecision();
214
222
  }
215
- if (["forward", "merge_forward", "delete", "urgent_app", "urgent_phone", "urgent_sms"]
223
+ if (["forward", "merge_forward", "delete", "urgent_phone", "urgent_sms"]
216
224
  .some((operation) => exactPath(parsed.commandArgv, ["im", "messages", operation]))) {
217
225
  const expected = `raw-${parsed.commandArgv[2]}`;
218
226
  return uniqueProtectedOperation(protectedPaths, expected)
@@ -436,8 +444,9 @@ function guardedTarget(decision, argv, store) {
436
444
  if (decision.operation === "send") {
437
445
  const chatId = policyFlagValue(argv, "--chat-id");
438
446
  const userId = policyFlagValue(argv, "--user-id");
439
- if (!chatId || userId)
447
+ if (!chatId || userId) {
440
448
  throw new Error("Runtime +messages-send 必须只使用 Inbox 已确认的 --chat-id;--user-id 无法建立 freshness target");
449
+ }
441
450
  return feishuImTarget(`chat:${chatId}`);
442
451
  }
443
452
  const messageId = policyFlagValue(argv, "--message-id");
@@ -448,7 +457,62 @@ function guardedTarget(decision, argv, store) {
448
457
  throw new Error(`无法从 Inbox 状态确定 ${messageId} 的 target;先 poll 对应消息,禁止旁路 freshness`);
449
458
  return feishuImTarget(target);
450
459
  }
451
- function botArgv(argv, intentId) {
460
+ function uniqueRawFlagValue(argv, flag) {
461
+ const nativeArgv = nativeArgvBeforeBoundary(argv);
462
+ const values = [];
463
+ for (let index = 0; index < nativeArgv.length; index += 1) {
464
+ const argument = nativeArgv[index];
465
+ if (argument === flag) {
466
+ values.push(nativeArgv[index + 1] ?? "");
467
+ index += 1;
468
+ continue;
469
+ }
470
+ if (argument.startsWith(`${flag}=`))
471
+ values.push(argument.slice(flag.length + 1));
472
+ }
473
+ if (values.length > 1)
474
+ throw new Error(`Runtime im messages urgent_app 不允许重复 ${flag};官方 CLI 会采用最后一次赋值`);
475
+ return values[0] || null;
476
+ }
477
+ function resolveUrgentAppTarget(argv, env, io, dependencies) {
478
+ const messageId = policyFlagValue(argv, "--message-id");
479
+ if (!messageId || !/^om_/.test(messageId)) {
480
+ throw new Error("Runtime im messages urgent_app 只接受真实 Feishu om_ message_id");
481
+ }
482
+ const result = callNative([
483
+ "im", "+messages-mget",
484
+ "--message-ids", messageId,
485
+ "--no-reactions",
486
+ "--json",
487
+ "--as", "bot",
488
+ ], env, io, dependencies);
489
+ if (result.error)
490
+ throw new Error(`urgent_app message lookup failed: ${result.error.message}`);
491
+ if (result.status !== 0)
492
+ throw new Error(`urgent_app message lookup exited ${result.status ?? "without status"}: ${result.stderr || "no details"}`);
493
+ let value;
494
+ try {
495
+ value = JSON.parse(result.stdout || "");
496
+ }
497
+ catch {
498
+ throw new Error("urgent_app message lookup returned non-JSON output");
499
+ }
500
+ const root = value;
501
+ if (!root || root.ok !== true || !root.data)
502
+ throw new Error("urgent_app message lookup returned an unsuccessful payload");
503
+ if (root.identity !== "bot")
504
+ throw new Error("urgent_app message lookup did not confirm Bot identity");
505
+ const rows = root.data.messages;
506
+ if (!Array.isArray(rows))
507
+ throw new Error("urgent_app message lookup omitted messages");
508
+ const message = rows.find((row) => row && typeof row === "object" && !Array.isArray(row)
509
+ && row.message_id === messageId);
510
+ if (!message || typeof message.chat_id !== "string" || !message.chat_id) {
511
+ throw new Error(`无法从官方 +messages-mget 确认 ${messageId} 的 chat;禁止旁路加急`);
512
+ }
513
+ return { target: feishuImTarget(`chat:${message.chat_id}`), message };
514
+ }
515
+ function botArgv(argv, intentId, decision) {
452
516
  const next = [...argv];
453
517
  const parsed = parsePolicyArgv(next);
454
518
  const boundary = next.indexOf("--");
@@ -456,10 +520,145 @@ function botArgv(argv, intentId) {
456
520
  const injected = [];
457
521
  if (!parsed.flags.has("--as"))
458
522
  injected.push("--as", "bot");
459
- if (!parsed.flags.has("--idempotency-key"))
523
+ if (decision?.operation !== "urgent-app" && !parsed.flags.has("--idempotency-key"))
460
524
  injected.push("--idempotency-key", intentId);
461
525
  next.splice(insertion, 0, ...injected);
462
- return next;
526
+ if (decision?.operation !== "urgent-app")
527
+ return next;
528
+ const rewritten = [];
529
+ for (let index = 0; index < next.length; index += 1) {
530
+ const argument = next[index];
531
+ if (argument === "--idempotency-key") {
532
+ index += 1;
533
+ continue;
534
+ }
535
+ if (argument.startsWith("--idempotency-key="))
536
+ continue;
537
+ rewritten.push(argument);
538
+ }
539
+ return rewritten;
540
+ }
541
+ function parseJsonObject(value, label) {
542
+ let parsed;
543
+ try {
544
+ parsed = JSON.parse(value);
545
+ }
546
+ catch {
547
+ throw new Error(`${label} 必须是 JSON 对象`);
548
+ }
549
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
550
+ throw new Error(`${label} 必须是 JSON 对象`);
551
+ return parsed;
552
+ }
553
+ function senderRecord(message) {
554
+ const sender = message.sender;
555
+ return sender && typeof sender === "object" && !Array.isArray(sender) ? sender : null;
556
+ }
557
+ function isOwnBotMessage(message, feishuAppId) {
558
+ const sender = senderRecord(message);
559
+ if (!sender)
560
+ return false;
561
+ const senderType = sender.sender_type;
562
+ const senderId = sender.id ?? sender.sender_id;
563
+ return senderType === "app" && senderId === feishuAppId;
564
+ }
565
+ function assertUrgentAppPreconditions(argv, message, target, feishuAppId) {
566
+ const messageId = policyFlagValue(argv, "--message-id");
567
+ if (!messageId || !/^om_/.test(messageId) || message.message_id !== messageId) {
568
+ throw new Error("Runtime im messages urgent_app 只接受真实 Feishu om_ message_id");
569
+ }
570
+ const userIdType = uniqueRawFlagValue(argv, "--user-id-type");
571
+ if (userIdType !== "open_id")
572
+ throw new Error("Runtime im messages urgent_app 必须使用 --user-id-type open_id");
573
+ const data = uniqueRawFlagValue(argv, "--data");
574
+ if (!data)
575
+ throw new Error("Runtime im messages urgent_app 缺少 --data");
576
+ const body = parseJsonObject(data, "--data");
577
+ const userIds = body.user_id_list;
578
+ if (!Array.isArray(userIds) || userIds.length === 0 || userIds.some((value) => typeof value !== "string" || !value.startsWith("ou_"))) {
579
+ throw new Error("Runtime im messages urgent_app 的 user_id_list 必须是非空 open_id 列表");
580
+ }
581
+ if (target.resourceKind === "chat" && message.chat_id && message.chat_id !== target.resourceId) {
582
+ throw new Error("加急目标消息不属于当前 chat freshness target");
583
+ }
584
+ if (!isOwnBotMessage(message, feishuAppId))
585
+ throw new Error("Runtime im messages urgent_app 只能加急当前 Bot 自己发出的消息");
586
+ return userIds.filter((value) => typeof value === "string");
587
+ }
588
+ function parseChatMemberOpenIds(result) {
589
+ if (result.error)
590
+ throw new Error(`urgent-app member probe failed: ${result.error.message}`);
591
+ if (result.status !== 0)
592
+ throw new Error(`urgent-app member probe exited ${result.status ?? "without status"}: ${result.stderr || "no details"}`);
593
+ let value;
594
+ try {
595
+ value = JSON.parse(result.stdout || "");
596
+ }
597
+ catch {
598
+ throw new Error("urgent-app member probe returned non-JSON output");
599
+ }
600
+ const root = value;
601
+ if (!root || root.ok !== true || !root.data)
602
+ throw new Error("urgent-app member probe returned an unsuccessful payload");
603
+ if (root.identity !== "bot")
604
+ throw new Error("urgent-app member probe did not confirm Bot identity");
605
+ if (root.data.has_more === true) {
606
+ throw new Error("urgent-app member probe is incomplete; refusing to guess chat membership");
607
+ }
608
+ if (Array.isArray(root.data.truncations) && root.data.truncations.length > 0) {
609
+ throw new Error("urgent-app member probe was truncated; refusing to guess chat membership");
610
+ }
611
+ const users = root.data.users;
612
+ if (!Array.isArray(users))
613
+ throw new Error("urgent-app member probe omitted users");
614
+ const ids = [];
615
+ for (const user of users) {
616
+ if (!user || typeof user !== "object" || Array.isArray(user))
617
+ continue;
618
+ const memberId = user.member_id;
619
+ if (typeof memberId === "string" && memberId.startsWith("ou_"))
620
+ ids.push(memberId);
621
+ }
622
+ return ids;
623
+ }
624
+ function assertUrgentAppMembers(argv, target, userIds, env, io, dependencies) {
625
+ if (target.resourceKind !== "chat" || !target.resourceId) {
626
+ throw new Error("Runtime im messages urgent_app 只能对照已确认的 chat freshness target 校验成员");
627
+ }
628
+ const members = new Set(parseChatMemberOpenIds(callNative([
629
+ "im", "+chat-members-list",
630
+ "--chat-id", target.resourceId,
631
+ "--member-id-type", "open_id",
632
+ "--member-types", "user",
633
+ "--page-all",
634
+ "--page-limit", "0",
635
+ "--json",
636
+ "--as", "bot",
637
+ ], env, io, dependencies)));
638
+ const unknown = userIds.filter((userId) => !members.has(userId));
639
+ if (unknown.length > 0) {
640
+ throw new Error(`Runtime im messages urgent_app 拒绝非本会话成员: ${unknown.join(",")}`);
641
+ }
642
+ }
643
+ function assertUrgentAppNativeAccepted(result) {
644
+ if (result.error || result.status !== 0)
645
+ return;
646
+ let value;
647
+ try {
648
+ value = JSON.parse(result.stdout || "");
649
+ }
650
+ catch {
651
+ return;
652
+ }
653
+ if (!value || typeof value !== "object" || Array.isArray(value))
654
+ return;
655
+ const data = value.data;
656
+ if (!data || typeof data !== "object" || Array.isArray(data))
657
+ return;
658
+ const invalid = data.invalid_user_id_list;
659
+ if (!Array.isArray(invalid) || invalid.length === 0)
660
+ return;
661
+ throw new Error(`Runtime im messages urgent_app 收到 invalid_user_id_list,已 fail-closed: ${invalid.join(",")}`);
463
662
  }
464
663
  function probeArgv(target) {
465
664
  if (target.provider !== "feishu.im")
@@ -840,7 +1039,10 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
840
1039
  if (decision.kind === "passthrough")
841
1040
  return passthroughWithObservation(effectiveArgv, privateEnv, io, nativeDependencies, store);
842
1041
  try {
843
- const target = guardedTarget(decision, effectiveArgv, store);
1042
+ const urgent = decision.operation === "urgent-app"
1043
+ ? resolveUrgentAppTarget(effectiveArgv, privateEnv, io, nativeDependencies)
1044
+ : null;
1045
+ const target = urgent?.target ?? guardedTarget(decision, effectiveArgv, store);
844
1046
  const targetKey = serializeFeishuImTarget(target);
845
1047
  const generation = freshnessGeneration(privateEnv);
846
1048
  const seen = store.readFreshnessCursor(targetKey, generation);
@@ -858,8 +1060,27 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
858
1060
  store.mergeFreshnessCursor(targetKey, gated.current, mergeFeishuImCursor, generation);
859
1061
  return 3;
860
1062
  }
1063
+ if (decision.operation === "urgent-app") {
1064
+ const userIds = assertUrgentAppPreconditions(effectiveArgv, urgent.message, target, agent.feishuAppId);
1065
+ assertUrgentAppMembers(effectiveArgv, target, userIds, privateEnv, io, nativeDependencies);
1066
+ }
861
1067
  const intentKey = policyFlagValue(effectiveArgv, "--idempotency-key") ?? intentId(targetKey, effectiveArgv);
862
- const write = callNative(botArgv(effectiveArgv, intentKey), privateEnv, io, nativeDependencies);
1068
+ const write = callNative(botArgv(effectiveArgv, intentKey, decision), privateEnv, io, nativeDependencies);
1069
+ if (decision.operation === "urgent-app") {
1070
+ try {
1071
+ assertUrgentAppNativeAccepted(write);
1072
+ }
1073
+ catch (error) {
1074
+ io.stderr(`${JSON.stringify({
1075
+ ok: false,
1076
+ identity: "bot",
1077
+ committed: true,
1078
+ error: { type: "validation", message: error instanceof Error ? error.message : String(error) },
1079
+ })}\n`);
1080
+ return 2;
1081
+ }
1082
+ return emitNativeResult(write, io);
1083
+ }
863
1084
  const writeMessage = writeResponseMessage(write);
864
1085
  const duplicate = !write.error && write.status === 0 && writeMessage
865
1086
  ? recordImWriteMemo(store, intentKey, writeMessage.message_id) : false;
@@ -1480,7 +1480,6 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1480
1480
  throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
1481
1481
  if (initialProjection.reconnecting)
1482
1482
  throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
1483
- markRuntimeTransition(agent, "Runtime session recovery in progress");
1484
1483
  let recovery;
1485
1484
  try {
1486
1485
  recovery = await runtimeHost.recoverSession(agentId, reason);
@@ -1440,7 +1440,6 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1440
1440
  throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
1441
1441
  if (initialProjection.reconnecting)
1442
1442
  throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
1443
- markRuntimeTransition(agent, "Runtime session recovery in progress");
1444
1443
  let recovery;
1445
1444
  try {
1446
1445
  recovery = await runtimeHost.recoverSession(agentId, reason);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [