larkin 0.2.70

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 (144) hide show
  1. package/CONTRIBUTING.md +48 -0
  2. package/LICENSE +201 -0
  3. package/README.md +122 -0
  4. package/SECURITY.md +14 -0
  5. package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
  6. package/dist/agent/agent-cli-capabilities.cjs +43 -0
  7. package/dist/agent/agent-cli-capabilities.mjs +35 -0
  8. package/dist/agent/agent-state-store.cjs +961 -0
  9. package/dist/agent/agent-state-store.mjs +923 -0
  10. package/dist/agent/agent-transport.cjs +201 -0
  11. package/dist/agent/agent-transport.mjs +194 -0
  12. package/dist/agent/config-cli-contract.cjs +19 -0
  13. package/dist/agent/config-cli-contract.mjs +16 -0
  14. package/dist/agent/context-prompt.cjs +159 -0
  15. package/dist/agent/context-prompt.mjs +155 -0
  16. package/dist/agent/freshness-gate.mjs +24 -0
  17. package/dist/agent/history-projection.cjs +72 -0
  18. package/dist/agent/history-projection.mjs +65 -0
  19. package/dist/agent/host-reminder-orchestrator.cjs +289 -0
  20. package/dist/agent/host-reminder-orchestrator.mjs +252 -0
  21. package/dist/agent/inbox-projection.cjs +92 -0
  22. package/dist/agent/inbox-projection.mjs +85 -0
  23. package/dist/agent/interaction-state-machine.cjs +710 -0
  24. package/dist/agent/interaction-state-machine.mjs +705 -0
  25. package/dist/agent/reminder-routes.cjs +192 -0
  26. package/dist/agent/reminder-routes.mjs +156 -0
  27. package/dist/agent/reminder-store.cjs +220 -0
  28. package/dist/agent/reminder-store.mjs +205 -0
  29. package/dist/agent/transport-business-context.cjs +486 -0
  30. package/dist/agent/transport-business-context.mjs +446 -0
  31. package/dist/agent/transport-shell.cjs +45 -0
  32. package/dist/agent/transport-shell.mjs +39 -0
  33. package/dist/app/agent-cli.mjs +596 -0
  34. package/dist/app/agent-config.mjs +633 -0
  35. package/dist/app/agent-lark-cli-workspace.cjs +95 -0
  36. package/dist/app/agent-lark-cli-workspace.mjs +85 -0
  37. package/dist/app/agent-readiness.cjs +42 -0
  38. package/dist/app/agent-readiness.mjs +38 -0
  39. package/dist/app/binary-entry.mjs +86 -0
  40. package/dist/app/cli.mjs +176 -0
  41. package/dist/app/dashboard.mjs +205 -0
  42. package/dist/app/internal-command.cjs +57 -0
  43. package/dist/app/internal-command.mjs +48 -0
  44. package/dist/app/lark-channel-secret.mjs +22 -0
  45. package/dist/app/lark-cli.mjs +887 -0
  46. package/dist/app/lark.mjs +45 -0
  47. package/dist/app/local-control.mjs +728 -0
  48. package/dist/app/official-lark-cli.cjs +138 -0
  49. package/dist/app/official-lark-cli.mjs +128 -0
  50. package/dist/app/pi-auth-cli.mjs +67 -0
  51. package/dist/app/run.mjs +364 -0
  52. package/dist/app/runtime-agent-config.mjs +416 -0
  53. package/dist/app/runtime-model-directory.mjs +78 -0
  54. package/dist/app/runtime-process.mjs +113 -0
  55. package/dist/app/session-cli.mjs +83 -0
  56. package/dist/app/setup-dashboard.mjs +26 -0
  57. package/dist/app/setup.mjs +166 -0
  58. package/dist/app/telemetry.mjs +83 -0
  59. package/dist/dashboard/dashboard-assets.mjs +21 -0
  60. package/dist/dashboard/dashboard-avatar.mjs +128 -0
  61. package/dist/dashboard/dashboard-config-controller.mjs +525 -0
  62. package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
  63. package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
  64. package/dist/dashboard/dashboard-template.mjs +29 -0
  65. package/dist/dashboard/dashboard-view-model.mjs +556 -0
  66. package/dist/dashboard/dashboard-workspace.mjs +104 -0
  67. package/dist/dashboard/web/assets/dashboard.css +3 -0
  68. package/dist/dashboard/web/assets/dashboard.js +49 -0
  69. package/dist/feishu/channel-transport.cjs +151 -0
  70. package/dist/feishu/channel-transport.mjs +148 -0
  71. package/dist/feishu/document-comment.cjs +120 -0
  72. package/dist/feishu/document-comment.mjs +109 -0
  73. package/dist/feishu/host-business-state.cjs +477 -0
  74. package/dist/feishu/host-business-state.mjs +462 -0
  75. package/dist/feishu/host-channel-business.cjs +161 -0
  76. package/dist/feishu/host-channel-business.mjs +157 -0
  77. package/dist/feishu/host-processing-eye.cjs +185 -0
  78. package/dist/feishu/host-processing-eye.mjs +181 -0
  79. package/dist/feishu/host-shell.cjs +1425 -0
  80. package/dist/feishu/host-shell.mjs +1385 -0
  81. package/dist/feishu/im-freshness-adapter.mjs +89 -0
  82. package/dist/feishu/interaction-orchestrator.cjs +279 -0
  83. package/dist/feishu/interaction-orchestrator.mjs +242 -0
  84. package/dist/feishu/lark-passthrough.mjs +70 -0
  85. package/dist/feishu/message-policy.cjs +111 -0
  86. package/dist/feishu/message-policy.mjs +98 -0
  87. package/dist/feishu/outbound-transport.cjs +221 -0
  88. package/dist/feishu/outbound-transport.mjs +215 -0
  89. package/dist/platform/build-info.cjs +25 -0
  90. package/dist/platform/build-info.mjs +18 -0
  91. package/dist/platform/callback-capability.cjs +301 -0
  92. package/dist/platform/callback-capability.mjs +252 -0
  93. package/dist/platform/check-bun-version.cjs +12 -0
  94. package/dist/platform/config.cjs +751 -0
  95. package/dist/platform/config.mjs +694 -0
  96. package/dist/platform/process-inspect.cjs +89 -0
  97. package/dist/platform/process-state.cjs +237 -0
  98. package/dist/platform/process-state.mjs +223 -0
  99. package/dist/platform/release-artifacts.mjs +61 -0
  100. package/dist/platform/root-layout.cjs +111 -0
  101. package/dist/platform/root-layout.mjs +72 -0
  102. package/dist/platform/telemetry-config.cjs +64 -0
  103. package/dist/platform/telemetry-config.mjs +56 -0
  104. package/dist/platform/telemetry-spool.cjs +661 -0
  105. package/dist/platform/telemetry-spool.mjs +653 -0
  106. package/dist/platform/telemetry-tracing.cjs +602 -0
  107. package/dist/platform/telemetry-tracing.mjs +596 -0
  108. package/dist/platform/telemetry-uploader.cjs +150 -0
  109. package/dist/platform/telemetry-uploader.mjs +146 -0
  110. package/dist/platform/workspace-service.cjs +469 -0
  111. package/dist/platform/workspace-service.mjs +430 -0
  112. package/dist/runtime/builtin-pi-assets.mjs +33 -0
  113. package/dist/runtime/claude-model-catalog.cjs +102 -0
  114. package/dist/runtime/claude-model-catalog.mjs +99 -0
  115. package/dist/runtime/codex-model-catalog.cjs +108 -0
  116. package/dist/runtime/codex-model-catalog.mjs +105 -0
  117. package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
  118. package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
  119. package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
  120. package/dist/runtime/pi-model-catalog.cjs +116 -0
  121. package/dist/runtime/pi-model-catalog.mjs +75 -0
  122. package/dist/runtime/pi-official-auth.mjs +480 -0
  123. package/dist/runtime/pi-provider-config.cjs +252 -0
  124. package/dist/runtime/pi-provider-config.mjs +239 -0
  125. package/dist/runtime/pi-rpc-client.cjs +255 -0
  126. package/dist/runtime/pi-rpc-client.mjs +251 -0
  127. package/dist/runtime/pi-subagent-injection.mjs +129 -0
  128. package/dist/runtime/pi-subagents.bundle.js +8990 -0
  129. package/dist/runtime/runtime-adapters.mjs +1019 -0
  130. package/dist/runtime/runtime-contracts.cjs +2 -0
  131. package/dist/runtime/runtime-contracts.mjs +1 -0
  132. package/dist/runtime/runtime-host.cjs +930 -0
  133. package/dist/runtime/runtime-host.mjs +922 -0
  134. package/dist/runtime/runtime-model-catalog.cjs +43 -0
  135. package/dist/runtime/runtime-model-catalog.mjs +40 -0
  136. package/dist/runtime/runtime-readiness.cjs +177 -0
  137. package/dist/runtime/runtime-readiness.mjs +133 -0
  138. package/dist/setup/bot-register.mjs +655 -0
  139. package/dist/setup/grant-scopes.mjs +133 -0
  140. package/dist/setup/run-credential-preflight.mjs +89 -0
  141. package/dist/setup/setup-agent-choice.mjs +172 -0
  142. package/dist/setup/setup-bind.mjs +337 -0
  143. package/dist/setup/setup-binding.mjs +52 -0
  144. package/package.json +112 -0
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.decideWake = decideWake;
7
+ exports.normalizeChannelMessage = normalizeChannelMessage;
8
+ exports.slug10 = slug10;
9
+ exports.targetFor = targetFor;
10
+ exports.targetAliases = targetAliases;
11
+ exports.xmlText = xmlText;
12
+ exports.signatureDescription = signatureDescription;
13
+ exports.createMessageEnvelope = createMessageEnvelope;
14
+ const node_crypto_1 = __importDefault(require("node:crypto"));
15
+ function decideWake(input) {
16
+ if (input.senderIsBot) {
17
+ const wake = input.mentionedBot && !input.mentionAll;
18
+ return { wake, reason: wake ? "bot-exact-mention" : "bot-not-exactly-mentioned" };
19
+ }
20
+ if (!input.isGroup)
21
+ return { wake: true, reason: "human-direct-message" };
22
+ if (input.mentionedBot || input.mentionAll)
23
+ return { wake: true, reason: "human-mentioned" };
24
+ if (input.mentionPolicy === "free" || input.noMentionChat)
25
+ return { wake: true, reason: "human-no-mention-chat" };
26
+ return { wake: false, reason: "human-group-not-mentioned" };
27
+ }
28
+ function normalizeChannelMessage(message) {
29
+ const messageId = String(message.messageId || "");
30
+ return {
31
+ chat_id: String(message.chatId || ""),
32
+ chat_type: message.chatMode === "topic" ? "group" : String(message.chatType || "group"),
33
+ sender_id: String(message.senderId || ""),
34
+ message_id: messageId,
35
+ event_id: messageId,
36
+ content: String(message.content || ""),
37
+ create_time: message.createTime,
38
+ thread_id: message.threadId || null,
39
+ _mentioned_bot: Boolean(message.mentionedBot || message.mentionAll),
40
+ _mention_all: Boolean(message.mentionAll),
41
+ _sender_is_bot: message.senderIsBot === true,
42
+ };
43
+ }
44
+ function slug10(value) {
45
+ return String(value || "").replace(/[^a-zA-Z0-9]/g, "").slice(-10);
46
+ }
47
+ function targetFor(event) {
48
+ const slug = `c${slug10(event.chat_id)}`;
49
+ const isDm = event.chat_type === "p2p";
50
+ const base = isDm ? `dm:@${slug}` : `#${slug}`;
51
+ if (event.thread_id) {
52
+ const threadSlug = slug10(event.thread_id);
53
+ return {
54
+ target: `${base}:${threadSlug.slice(0, 8)}`,
55
+ channel_type: "thread",
56
+ channel_name: threadSlug,
57
+ parent_channel_type: isDm ? "dm" : "channel",
58
+ parent_channel_name: slug,
59
+ };
60
+ }
61
+ return { target: base, channel_type: isDm ? "dm" : "channel", channel_name: slug };
62
+ }
63
+ function targetAliases(event, target = targetFor(event)) {
64
+ const slug = `c${slug10(event.chat_id)}`;
65
+ const base = event.chat_type === "p2p" ? `dm:@${slug}` : `#${slug}`;
66
+ const aliases = [base];
67
+ if (target.channel_type === "thread" && target.channel_name) {
68
+ aliases.push(`${base}:${target.channel_name}`, `${base}:${target.channel_name.slice(0, 8)}`);
69
+ }
70
+ return [...new Set(aliases)];
71
+ }
72
+ function xmlText(value) {
73
+ return String(value)
74
+ .replace(/&/g, "&")
75
+ .replace(/</g, "&lt;")
76
+ .replace(/>/g, "&gt;")
77
+ .replace(/\"/g, "&quot;")
78
+ .replace(/'/g, "&apos;");
79
+ }
80
+ function signatureDescription(signature) {
81
+ return signature ? `<feishu_signature>${xmlText(signature)}</feishu_signature>` : null;
82
+ }
83
+ function createMessageEnvelope({ agent, event, seq, names = {}, signature = null, }) {
84
+ if (!Number.isSafeInteger(seq) || seq < 1)
85
+ throw new Error(`invalid message sequence: ${seq}`);
86
+ const target = targetFor(event);
87
+ const senderIsBot = Boolean(agent.botOpenId && event.sender_id === agent.botOpenId) || event._sender_is_bot;
88
+ const senderDescription = signatureDescription(signature);
89
+ const timestamp = event.create_time
90
+ ? new Date(Number(event.create_time)).toISOString()
91
+ : new Date().toISOString();
92
+ let content = event.content || "";
93
+ if (event._mentioned_bot && !event._mention_all) {
94
+ content += `\n[提及说明: 本消息 @ 的${agent.botName ? `“${agent.botName}”` : "机器人"}就是你自己,请按被点名处理]`;
95
+ }
96
+ return {
97
+ message_id: event.message_id || `m_${node_crypto_1.default.randomBytes(6).toString("hex")}`,
98
+ seq,
99
+ sender_id: event.sender_id || null,
100
+ sender_name: names[event.sender_id] || event.sender_id || "user",
101
+ ...(senderDescription ? { sender_description: senderDescription } : {}),
102
+ sender_type: senderIsBot ? "agent" : "human",
103
+ channel_type: target.channel_type,
104
+ channel_name: target.channel_name,
105
+ content,
106
+ timestamp,
107
+ thread_id: event.thread_id || null,
108
+ ...(target.parent_channel_type ? { parent_channel_type: target.parent_channel_type } : {}),
109
+ ...(target.parent_channel_name ? { parent_channel_name: target.parent_channel_name } : {}),
110
+ };
111
+ }
@@ -0,0 +1,98 @@
1
+ import crypto from "node:crypto";
2
+ export function decideWake(input) {
3
+ if (input.senderIsBot) {
4
+ const wake = input.mentionedBot && !input.mentionAll;
5
+ return { wake, reason: wake ? "bot-exact-mention" : "bot-not-exactly-mentioned" };
6
+ }
7
+ if (!input.isGroup)
8
+ return { wake: true, reason: "human-direct-message" };
9
+ if (input.mentionedBot || input.mentionAll)
10
+ return { wake: true, reason: "human-mentioned" };
11
+ if (input.mentionPolicy === "free" || input.noMentionChat)
12
+ return { wake: true, reason: "human-no-mention-chat" };
13
+ return { wake: false, reason: "human-group-not-mentioned" };
14
+ }
15
+ export function normalizeChannelMessage(message) {
16
+ const messageId = String(message.messageId || "");
17
+ return {
18
+ chat_id: String(message.chatId || ""),
19
+ chat_type: message.chatMode === "topic" ? "group" : String(message.chatType || "group"),
20
+ sender_id: String(message.senderId || ""),
21
+ message_id: messageId,
22
+ event_id: messageId,
23
+ content: String(message.content || ""),
24
+ create_time: message.createTime,
25
+ thread_id: message.threadId || null,
26
+ _mentioned_bot: Boolean(message.mentionedBot || message.mentionAll),
27
+ _mention_all: Boolean(message.mentionAll),
28
+ _sender_is_bot: message.senderIsBot === true,
29
+ };
30
+ }
31
+ export function slug10(value) {
32
+ return String(value || "").replace(/[^a-zA-Z0-9]/g, "").slice(-10);
33
+ }
34
+ export function targetFor(event) {
35
+ const slug = `c${slug10(event.chat_id)}`;
36
+ const isDm = event.chat_type === "p2p";
37
+ const base = isDm ? `dm:@${slug}` : `#${slug}`;
38
+ if (event.thread_id) {
39
+ const threadSlug = slug10(event.thread_id);
40
+ return {
41
+ target: `${base}:${threadSlug.slice(0, 8)}`,
42
+ channel_type: "thread",
43
+ channel_name: threadSlug,
44
+ parent_channel_type: isDm ? "dm" : "channel",
45
+ parent_channel_name: slug,
46
+ };
47
+ }
48
+ return { target: base, channel_type: isDm ? "dm" : "channel", channel_name: slug };
49
+ }
50
+ export function targetAliases(event, target = targetFor(event)) {
51
+ const slug = `c${slug10(event.chat_id)}`;
52
+ const base = event.chat_type === "p2p" ? `dm:@${slug}` : `#${slug}`;
53
+ const aliases = [base];
54
+ if (target.channel_type === "thread" && target.channel_name) {
55
+ aliases.push(`${base}:${target.channel_name}`, `${base}:${target.channel_name.slice(0, 8)}`);
56
+ }
57
+ return [...new Set(aliases)];
58
+ }
59
+ export function xmlText(value) {
60
+ return String(value)
61
+ .replace(/&/g, "&amp;")
62
+ .replace(/</g, "&lt;")
63
+ .replace(/>/g, "&gt;")
64
+ .replace(/\"/g, "&quot;")
65
+ .replace(/'/g, "&apos;");
66
+ }
67
+ export function signatureDescription(signature) {
68
+ return signature ? `<feishu_signature>${xmlText(signature)}</feishu_signature>` : null;
69
+ }
70
+ export function createMessageEnvelope({ agent, event, seq, names = {}, signature = null, }) {
71
+ if (!Number.isSafeInteger(seq) || seq < 1)
72
+ throw new Error(`invalid message sequence: ${seq}`);
73
+ const target = targetFor(event);
74
+ const senderIsBot = Boolean(agent.botOpenId && event.sender_id === agent.botOpenId) || event._sender_is_bot;
75
+ const senderDescription = signatureDescription(signature);
76
+ const timestamp = event.create_time
77
+ ? new Date(Number(event.create_time)).toISOString()
78
+ : new Date().toISOString();
79
+ let content = event.content || "";
80
+ if (event._mentioned_bot && !event._mention_all) {
81
+ content += `\n[提及说明: 本消息 @ 的${agent.botName ? `“${agent.botName}”` : "机器人"}就是你自己,请按被点名处理]`;
82
+ }
83
+ return {
84
+ message_id: event.message_id || `m_${crypto.randomBytes(6).toString("hex")}`,
85
+ seq,
86
+ sender_id: event.sender_id || null,
87
+ sender_name: names[event.sender_id] || event.sender_id || "user",
88
+ ...(senderDescription ? { sender_description: senderDescription } : {}),
89
+ sender_type: senderIsBot ? "agent" : "human",
90
+ channel_type: target.channel_type,
91
+ channel_name: target.channel_name,
92
+ content,
93
+ timestamp,
94
+ thread_id: event.thread_id || null,
95
+ ...(target.parent_channel_type ? { parent_channel_type: target.parent_channel_type } : {}),
96
+ ...(target.parent_channel_name ? { parent_channel_name: target.parent_channel_name } : {}),
97
+ };
98
+ }
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createOutboundTransport = createOutboundTransport;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ function createOutboundTransport(options) {
11
+ const stateRoot = node_path_1.default.resolve(node_path_1.default.dirname(options.attachmentIndexFile));
12
+ const attachmentDir = node_path_1.default.resolve(options.attachmentDir);
13
+ if (node_path_1.default.dirname(attachmentDir) !== stateRoot)
14
+ throw new Error("attachment directory must be an Agent-state child");
15
+ const rejectSymlink = (target) => {
16
+ try {
17
+ if (node_fs_1.default.lstatSync(target).isSymbolicLink())
18
+ throw new Error(`attachment path must not be a symlink: ${target}`);
19
+ }
20
+ catch (error) {
21
+ if (error.code !== "ENOENT")
22
+ throw error;
23
+ }
24
+ };
25
+ const ensureAttachmentDirectory = () => {
26
+ rejectSymlink(stateRoot);
27
+ rejectSymlink(attachmentDir);
28
+ node_fs_1.default.mkdirSync(attachmentDir, { recursive: true, mode: 0o700 });
29
+ const stat = node_fs_1.default.lstatSync(attachmentDir);
30
+ if (!stat.isDirectory() || stat.isSymbolicLink()
31
+ || (typeof process.getuid === "function" && stat.uid !== process.getuid())) {
32
+ throw new Error("attachment directory security attributes are invalid");
33
+ }
34
+ node_fs_1.default.chmodSync(attachmentDir, 0o700);
35
+ };
36
+ const withIndexLock = (operation) => {
37
+ const lock = `${options.attachmentIndexFile}.lock`;
38
+ const deadline = Date.now() + 2_000;
39
+ for (;;) {
40
+ try {
41
+ node_fs_1.default.mkdirSync(lock, { mode: 0o700 });
42
+ break;
43
+ }
44
+ catch (error) {
45
+ if (error.code !== "EEXIST")
46
+ throw error;
47
+ try {
48
+ if (Date.now() - node_fs_1.default.statSync(lock).mtimeMs > 10_000) {
49
+ node_fs_1.default.rmdirSync(lock);
50
+ continue;
51
+ }
52
+ }
53
+ catch { /* another process may be reclaiming the stale lock */ }
54
+ if (Date.now() > deadline)
55
+ throw new Error("attachment index lock timeout");
56
+ const until = Date.now() + 15;
57
+ while (Date.now() < until) { /* bounded synchronous cross-process lock */ }
58
+ }
59
+ }
60
+ try {
61
+ return operation();
62
+ }
63
+ finally {
64
+ try {
65
+ node_fs_1.default.rmdirSync(lock);
66
+ }
67
+ catch { /* crash recovery reclaims stale locks */ }
68
+ }
69
+ };
70
+ const loadAttachmentIndex = () => {
71
+ try {
72
+ rejectSymlink(options.attachmentIndexFile);
73
+ return JSON.parse(node_fs_1.default.readFileSync(options.attachmentIndexFile, "utf8"));
74
+ }
75
+ catch {
76
+ return {};
77
+ }
78
+ };
79
+ const saveAttachmentIndex = (index) => {
80
+ if (!options.attachmentIndexFile)
81
+ return;
82
+ rejectSymlink(options.attachmentIndexFile);
83
+ const temporary = `${options.attachmentIndexFile}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`;
84
+ let descriptor;
85
+ try {
86
+ descriptor = node_fs_1.default.openSync(temporary, node_fs_1.default.constants.O_WRONLY | node_fs_1.default.constants.O_CREAT | node_fs_1.default.constants.O_EXCL | node_fs_1.default.constants.O_NOFOLLOW, 0o600);
87
+ node_fs_1.default.writeFileSync(descriptor, `${JSON.stringify(index)}\n`);
88
+ node_fs_1.default.fsyncSync(descriptor);
89
+ node_fs_1.default.closeSync(descriptor);
90
+ descriptor = undefined;
91
+ node_fs_1.default.renameSync(temporary, options.attachmentIndexFile);
92
+ node_fs_1.default.chmodSync(options.attachmentIndexFile, 0o600);
93
+ }
94
+ finally {
95
+ if (descriptor !== undefined)
96
+ node_fs_1.default.closeSync(descriptor);
97
+ try {
98
+ node_fs_1.default.unlinkSync(temporary);
99
+ }
100
+ catch (error) {
101
+ if (error.code !== "ENOENT")
102
+ throw error;
103
+ }
104
+ }
105
+ };
106
+ const stageUpload = async (form) => {
107
+ const candidate = form && typeof form.get === "function" ? form.get("file") : null;
108
+ const file = candidate;
109
+ if (!file || typeof file.arrayBuffer !== "function")
110
+ return { ok: false, status: 400, error: "multipart 缺少 file 字段" };
111
+ const buffer = Buffer.from(await file.arrayBuffer());
112
+ const filename = String(file.name || form?.get("filename") || "upload.bin");
113
+ const mimeType = String(form?.get("mimeType") || file.type || "application/octet-stream");
114
+ const id = `att_${node_crypto_1.default.createHash("sha256").update(buffer).update(filename).digest("hex").slice(0, 20)}`;
115
+ try {
116
+ ensureAttachmentDirectory();
117
+ }
118
+ catch (error) {
119
+ return { ok: false, status: 500, error: `附件目录不安全: ${error.message}` };
120
+ }
121
+ const safe = filename.replace(/[^\w.\-]+/g, "_").slice(0, 80) || "file.bin";
122
+ const stored = `${id}__${safe}`;
123
+ const destination = node_path_1.default.join(attachmentDir, stored);
124
+ const temporary = node_path_1.default.join(attachmentDir, `.${id}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`);
125
+ try {
126
+ rejectSymlink(destination);
127
+ node_fs_1.default.writeFileSync(temporary, buffer, { mode: 0o600, flag: "wx" });
128
+ node_fs_1.default.renameSync(temporary, destination);
129
+ node_fs_1.default.chmodSync(destination, 0o600);
130
+ }
131
+ catch (error) {
132
+ return { ok: false, status: 500, error: `附件落盘失败: ${error.message}` };
133
+ }
134
+ finally {
135
+ try {
136
+ node_fs_1.default.unlinkSync(temporary);
137
+ }
138
+ catch (error) {
139
+ if (error.code !== "ENOENT")
140
+ throw error;
141
+ }
142
+ }
143
+ try {
144
+ withIndexLock(() => {
145
+ const index = loadAttachmentIndex();
146
+ index[id] = { file: stored, filename, mimeType, sizeBytes: buffer.length };
147
+ saveAttachmentIndex(index);
148
+ });
149
+ }
150
+ catch (error) {
151
+ return { ok: false, status: 500, error: `附件索引写入失败: ${error.message}` };
152
+ }
153
+ options.log(`attachment staged id=${id} name=${filename} bytes=${buffer.length}`);
154
+ return { ok: true, status: 200, data: { id, filename, mimeType, sizeBytes: buffer.length, thumbnailUrl: null } };
155
+ };
156
+ const handleSend = (body) => {
157
+ const target = body.target || "";
158
+ const content = String(body.content ?? "").replace(/\n+$/, "");
159
+ const chatId = options.resolveChatId(target);
160
+ if (!chatId) {
161
+ return { ok: false, status: 400, error: `该 target 没有已知 chat_id:${JSON.stringify(target)}。只能回复 canonical Inbox 给出的确切会话;不会兜底发往默认群。` };
162
+ }
163
+ const idempotencyKey = body.idempotencyKey
164
+ || node_crypto_1.default.createHash("sha256").update(`${String(target)}\0${content}`).digest("hex").slice(0, 32);
165
+ const replyContext = options.replyContextFor(String(target)) || options.replyContextFor(chatId);
166
+ const attachmentIds = Array.isArray(body.attachmentIds) ? body.attachmentIds.filter(Boolean) : [];
167
+ let messageId = "sent";
168
+ if (content !== "" || attachmentIds.length === 0) {
169
+ const replyInThread = Boolean(replyContext?.in_topic && replyContext.reply_to);
170
+ const result = replyInThread
171
+ ? options.replyText(replyContext.reply_to, content, idempotencyKey, chatId)
172
+ : options.sendText(chatId, content, idempotencyKey);
173
+ if (result.code !== 0) {
174
+ options.log("send 失败 FULL:", `${result.stdout || ""} || ${result.stderr || ""}`);
175
+ return { ok: false, status: 502, error: `lark-cli send failed: ${(result.stdout || result.stderr || "").trim().split("\n")[0]}` };
176
+ }
177
+ try {
178
+ const parsed = JSON.parse(result.stdout);
179
+ messageId = parsed?.data?.message_id || parsed?.message_id || (options.dryRun ? "dryrun" : "sent");
180
+ }
181
+ catch {
182
+ messageId = options.dryRun ? "dryrun" : "sent";
183
+ }
184
+ options.log(`${replyInThread ? "reply-in-thread" : "send"} → chat=${chatId} len=${content.length} id=${messageId}${options.dryRun ? " (dry-run)" : ""}`);
185
+ }
186
+ if (attachmentIds.length) {
187
+ const index = loadAttachmentIndex();
188
+ for (let position = 0; position < attachmentIds.length; position += 1) {
189
+ const attachmentId = String(attachmentIds[position]);
190
+ const attachment = index[attachmentId];
191
+ if (!attachment) {
192
+ options.log(`attachment 未找到 id=${attachmentId}(跳过)`);
193
+ continue;
194
+ }
195
+ const result = options.sendMedia(chatId, replyContext, attachment, `${idempotencyKey}:a${position}`);
196
+ if (result.code !== 0) {
197
+ options.log("attachment 发送失败 FULL:", `${result.stdout || ""} || ${result.stderr || ""}`);
198
+ return { ok: false, status: 502, error: `lark-cli 发送附件失败: ${(result.stdout || result.stderr || "").trim().split("\n")[0]}` };
199
+ }
200
+ try {
201
+ const parsed = JSON.parse(result.stdout);
202
+ messageId = parsed?.data?.message_id || messageId;
203
+ }
204
+ catch { /* retain prior message id */ }
205
+ options.log(`attach → chat=${chatId} name=${attachment.filename} id=${messageId}${options.dryRun ? " (dry-run)" : ""}`);
206
+ }
207
+ }
208
+ options.appendConversation({
209
+ direction: "out",
210
+ from: options.botDisplayName() || options.agentName,
211
+ senderType: "agent",
212
+ target,
213
+ wake: false,
214
+ text: content || `[发送了 ${attachmentIds.length} 个附件]`,
215
+ messageId,
216
+ at: new Date().toISOString(),
217
+ });
218
+ return { ok: true, status: 200, data: { ok: true, state: "sent", messageId, messageSeq: 1 } };
219
+ };
220
+ return { handleSend, stageUpload };
221
+ }
@@ -0,0 +1,215 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export function createOutboundTransport(options) {
5
+ const stateRoot = path.resolve(path.dirname(options.attachmentIndexFile));
6
+ const attachmentDir = path.resolve(options.attachmentDir);
7
+ if (path.dirname(attachmentDir) !== stateRoot)
8
+ throw new Error("attachment directory must be an Agent-state child");
9
+ const rejectSymlink = (target) => {
10
+ try {
11
+ if (fs.lstatSync(target).isSymbolicLink())
12
+ throw new Error(`attachment path must not be a symlink: ${target}`);
13
+ }
14
+ catch (error) {
15
+ if (error.code !== "ENOENT")
16
+ throw error;
17
+ }
18
+ };
19
+ const ensureAttachmentDirectory = () => {
20
+ rejectSymlink(stateRoot);
21
+ rejectSymlink(attachmentDir);
22
+ fs.mkdirSync(attachmentDir, { recursive: true, mode: 0o700 });
23
+ const stat = fs.lstatSync(attachmentDir);
24
+ if (!stat.isDirectory() || stat.isSymbolicLink()
25
+ || (typeof process.getuid === "function" && stat.uid !== process.getuid())) {
26
+ throw new Error("attachment directory security attributes are invalid");
27
+ }
28
+ fs.chmodSync(attachmentDir, 0o700);
29
+ };
30
+ const withIndexLock = (operation) => {
31
+ const lock = `${options.attachmentIndexFile}.lock`;
32
+ const deadline = Date.now() + 2_000;
33
+ for (;;) {
34
+ try {
35
+ fs.mkdirSync(lock, { mode: 0o700 });
36
+ break;
37
+ }
38
+ catch (error) {
39
+ if (error.code !== "EEXIST")
40
+ throw error;
41
+ try {
42
+ if (Date.now() - fs.statSync(lock).mtimeMs > 10_000) {
43
+ fs.rmdirSync(lock);
44
+ continue;
45
+ }
46
+ }
47
+ catch { /* another process may be reclaiming the stale lock */ }
48
+ if (Date.now() > deadline)
49
+ throw new Error("attachment index lock timeout");
50
+ const until = Date.now() + 15;
51
+ while (Date.now() < until) { /* bounded synchronous cross-process lock */ }
52
+ }
53
+ }
54
+ try {
55
+ return operation();
56
+ }
57
+ finally {
58
+ try {
59
+ fs.rmdirSync(lock);
60
+ }
61
+ catch { /* crash recovery reclaims stale locks */ }
62
+ }
63
+ };
64
+ const loadAttachmentIndex = () => {
65
+ try {
66
+ rejectSymlink(options.attachmentIndexFile);
67
+ return JSON.parse(fs.readFileSync(options.attachmentIndexFile, "utf8"));
68
+ }
69
+ catch {
70
+ return {};
71
+ }
72
+ };
73
+ const saveAttachmentIndex = (index) => {
74
+ if (!options.attachmentIndexFile)
75
+ return;
76
+ rejectSymlink(options.attachmentIndexFile);
77
+ const temporary = `${options.attachmentIndexFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
78
+ let descriptor;
79
+ try {
80
+ descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
81
+ fs.writeFileSync(descriptor, `${JSON.stringify(index)}\n`);
82
+ fs.fsyncSync(descriptor);
83
+ fs.closeSync(descriptor);
84
+ descriptor = undefined;
85
+ fs.renameSync(temporary, options.attachmentIndexFile);
86
+ fs.chmodSync(options.attachmentIndexFile, 0o600);
87
+ }
88
+ finally {
89
+ if (descriptor !== undefined)
90
+ fs.closeSync(descriptor);
91
+ try {
92
+ fs.unlinkSync(temporary);
93
+ }
94
+ catch (error) {
95
+ if (error.code !== "ENOENT")
96
+ throw error;
97
+ }
98
+ }
99
+ };
100
+ const stageUpload = async (form) => {
101
+ const candidate = form && typeof form.get === "function" ? form.get("file") : null;
102
+ const file = candidate;
103
+ if (!file || typeof file.arrayBuffer !== "function")
104
+ return { ok: false, status: 400, error: "multipart 缺少 file 字段" };
105
+ const buffer = Buffer.from(await file.arrayBuffer());
106
+ const filename = String(file.name || form?.get("filename") || "upload.bin");
107
+ const mimeType = String(form?.get("mimeType") || file.type || "application/octet-stream");
108
+ const id = `att_${crypto.createHash("sha256").update(buffer).update(filename).digest("hex").slice(0, 20)}`;
109
+ try {
110
+ ensureAttachmentDirectory();
111
+ }
112
+ catch (error) {
113
+ return { ok: false, status: 500, error: `附件目录不安全: ${error.message}` };
114
+ }
115
+ const safe = filename.replace(/[^\w.\-]+/g, "_").slice(0, 80) || "file.bin";
116
+ const stored = `${id}__${safe}`;
117
+ const destination = path.join(attachmentDir, stored);
118
+ const temporary = path.join(attachmentDir, `.${id}.${process.pid}.${crypto.randomUUID()}.tmp`);
119
+ try {
120
+ rejectSymlink(destination);
121
+ fs.writeFileSync(temporary, buffer, { mode: 0o600, flag: "wx" });
122
+ fs.renameSync(temporary, destination);
123
+ fs.chmodSync(destination, 0o600);
124
+ }
125
+ catch (error) {
126
+ return { ok: false, status: 500, error: `附件落盘失败: ${error.message}` };
127
+ }
128
+ finally {
129
+ try {
130
+ fs.unlinkSync(temporary);
131
+ }
132
+ catch (error) {
133
+ if (error.code !== "ENOENT")
134
+ throw error;
135
+ }
136
+ }
137
+ try {
138
+ withIndexLock(() => {
139
+ const index = loadAttachmentIndex();
140
+ index[id] = { file: stored, filename, mimeType, sizeBytes: buffer.length };
141
+ saveAttachmentIndex(index);
142
+ });
143
+ }
144
+ catch (error) {
145
+ return { ok: false, status: 500, error: `附件索引写入失败: ${error.message}` };
146
+ }
147
+ options.log(`attachment staged id=${id} name=${filename} bytes=${buffer.length}`);
148
+ return { ok: true, status: 200, data: { id, filename, mimeType, sizeBytes: buffer.length, thumbnailUrl: null } };
149
+ };
150
+ const handleSend = (body) => {
151
+ const target = body.target || "";
152
+ const content = String(body.content ?? "").replace(/\n+$/, "");
153
+ const chatId = options.resolveChatId(target);
154
+ if (!chatId) {
155
+ return { ok: false, status: 400, error: `该 target 没有已知 chat_id:${JSON.stringify(target)}。只能回复 canonical Inbox 给出的确切会话;不会兜底发往默认群。` };
156
+ }
157
+ const idempotencyKey = body.idempotencyKey
158
+ || crypto.createHash("sha256").update(`${String(target)}\0${content}`).digest("hex").slice(0, 32);
159
+ const replyContext = options.replyContextFor(String(target)) || options.replyContextFor(chatId);
160
+ const attachmentIds = Array.isArray(body.attachmentIds) ? body.attachmentIds.filter(Boolean) : [];
161
+ let messageId = "sent";
162
+ if (content !== "" || attachmentIds.length === 0) {
163
+ const replyInThread = Boolean(replyContext?.in_topic && replyContext.reply_to);
164
+ const result = replyInThread
165
+ ? options.replyText(replyContext.reply_to, content, idempotencyKey, chatId)
166
+ : options.sendText(chatId, content, idempotencyKey);
167
+ if (result.code !== 0) {
168
+ options.log("send 失败 FULL:", `${result.stdout || ""} || ${result.stderr || ""}`);
169
+ return { ok: false, status: 502, error: `lark-cli send failed: ${(result.stdout || result.stderr || "").trim().split("\n")[0]}` };
170
+ }
171
+ try {
172
+ const parsed = JSON.parse(result.stdout);
173
+ messageId = parsed?.data?.message_id || parsed?.message_id || (options.dryRun ? "dryrun" : "sent");
174
+ }
175
+ catch {
176
+ messageId = options.dryRun ? "dryrun" : "sent";
177
+ }
178
+ options.log(`${replyInThread ? "reply-in-thread" : "send"} → chat=${chatId} len=${content.length} id=${messageId}${options.dryRun ? " (dry-run)" : ""}`);
179
+ }
180
+ if (attachmentIds.length) {
181
+ const index = loadAttachmentIndex();
182
+ for (let position = 0; position < attachmentIds.length; position += 1) {
183
+ const attachmentId = String(attachmentIds[position]);
184
+ const attachment = index[attachmentId];
185
+ if (!attachment) {
186
+ options.log(`attachment 未找到 id=${attachmentId}(跳过)`);
187
+ continue;
188
+ }
189
+ const result = options.sendMedia(chatId, replyContext, attachment, `${idempotencyKey}:a${position}`);
190
+ if (result.code !== 0) {
191
+ options.log("attachment 发送失败 FULL:", `${result.stdout || ""} || ${result.stderr || ""}`);
192
+ return { ok: false, status: 502, error: `lark-cli 发送附件失败: ${(result.stdout || result.stderr || "").trim().split("\n")[0]}` };
193
+ }
194
+ try {
195
+ const parsed = JSON.parse(result.stdout);
196
+ messageId = parsed?.data?.message_id || messageId;
197
+ }
198
+ catch { /* retain prior message id */ }
199
+ options.log(`attach → chat=${chatId} name=${attachment.filename} id=${messageId}${options.dryRun ? " (dry-run)" : ""}`);
200
+ }
201
+ }
202
+ options.appendConversation({
203
+ direction: "out",
204
+ from: options.botDisplayName() || options.agentName,
205
+ senderType: "agent",
206
+ target,
207
+ wake: false,
208
+ text: content || `[发送了 ${attachmentIds.length} 个附件]`,
209
+ messageId,
210
+ at: new Date().toISOString(),
211
+ });
212
+ return { ok: true, status: 200, data: { ok: true, state: "sent", messageId, messageSeq: 1 } };
213
+ };
214
+ return { handleSend, stageUpload };
215
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.packageVersion = packageVersion;
7
+ exports.buildFingerprint = buildFingerprint;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const dashboard_lifecycle_js_1 = require("../dashboard/dashboard-lifecycle.cjs");
11
+ function packageVersion(root) {
12
+ if (typeof LARKIN_BUILD_VERSION !== "undefined" && LARKIN_BUILD_VERSION)
13
+ return LARKIN_BUILD_VERSION;
14
+ try {
15
+ return String(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(root, "package.json"), "utf8")).version || "dev");
16
+ }
17
+ catch {
18
+ return "dev";
19
+ }
20
+ }
21
+ function buildFingerprint(root) {
22
+ if (typeof LARKIN_BUILD_FINGERPRINT !== "undefined" && LARKIN_BUILD_FINGERPRINT)
23
+ return LARKIN_BUILD_FINGERPRINT;
24
+ return (0, dashboard_lifecycle_js_1.dashboardBuildFingerprint)(root);
25
+ }