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,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createChannelTransport = createChannelTransport;
4
+ function createChannelTransport(options) {
5
+ const handle = ({ path: requestPath, pathNoQuery, method, body }) => {
6
+ if (/\/channels$/.test(pathNoQuery) && method === "POST") {
7
+ const name = String(body.name || "").replace(/^#/, "").trim();
8
+ if (!name)
9
+ return { ok: false, status: 400, error: "建群需要 name" };
10
+ const allowPublic = process.env.LARKIN_ALLOW_PUBLIC_CHAT === "1";
11
+ const privateChat = !(allowPublic && body.visibility === "public");
12
+ const args = ["im", "+chat-create", "--name", name, "--chat-mode", "group", "--type", privateChat ? "private" : "public", "--json"];
13
+ if (body.description)
14
+ args.push("--description", String(body.description).slice(0, 100));
15
+ if (options.dryRun)
16
+ args.push("--dry-run");
17
+ const result = options.larkJsonOut(args);
18
+ if (!result || result.ok === false)
19
+ return { ok: false, status: 400, error: (result && options.feishuError(result)) || "建群失败" };
20
+ const chatId = (result.data && (result.data.chat_id || result.data.chatId)) || "";
21
+ if (chatId)
22
+ options.rememberChannel(`#${name}`, chatId, { name, chat_mode: "group", chat_type: "group" });
23
+ return { ok: true, status: 200, data: { id: `#${name}`, name, type: privateChat ? "private" : "public", joined: true, chatId: chatId || null } };
24
+ }
25
+ if (/\/channels\/[^/]+\/members$/.test(pathNoQuery) && (method === "POST" || method === "DELETE")) {
26
+ const match = pathNoQuery.match(/\/channels\/([^/]+)\/members$/);
27
+ const target = decodeURIComponent(match[1]);
28
+ const chatId = options.resolveChatId(target);
29
+ if (!chatId)
30
+ return { ok: false, status: 404, error: `未知频道 ${target}` };
31
+ const raw = body.user || body.agent || "";
32
+ const resolved = options.resolveMemberTo(chatId, raw);
33
+ if (!resolved)
34
+ return { ok: false, status: 400, error: `无法识别成员 "${raw}":请用 user_id(人) 或 app_id(cli_ 开头,bot);也支持 open_id(ou_)/union_id(on_)。可先跑 larkin channel members 查看各成员 id。` };
35
+ const verb = method === "DELETE" ? "delete" : "create";
36
+ const args = ["im", "chat.members", verb, "--chat-id", chatId, "--member-id-type", resolved.type, "--data", JSON.stringify({ id_list: resolved.ids }), "--json"];
37
+ if (verb === "delete")
38
+ args.push("--yes");
39
+ if (options.dryRun)
40
+ args.push("--dry-run");
41
+ const result = options.larkJsonOut(args);
42
+ if (result?.ok === false)
43
+ return { ok: false, status: 400, error: options.feishuError(result) || "成员操作失败" };
44
+ return { ok: true, status: 200, data: { member: { name: String(raw).replace(/^@/, "") || resolved.ids[0] } } };
45
+ }
46
+ if (/\/channels\/[^/]+\/join$/.test(pathNoQuery) && method === "POST") {
47
+ return { ok: false, status: 400, error: "飞书 bot 无法主动加入群聊;请让群主/管理员在群设置里邀请本 bot 进群。" };
48
+ }
49
+ if (/\/channels\/[^/]+\/leave$/.test(pathNoQuery) && method === "POST") {
50
+ const match = pathNoQuery.match(/\/channels\/([^/]+)\/leave$/);
51
+ const target = decodeURIComponent(match[1]);
52
+ const chatId = options.resolveChatId(target);
53
+ const selfApp = options.selectedAgent.feishuAppId;
54
+ if (!chatId || !selfApp)
55
+ return { ok: false, status: 400, error: "无法确定 chat_id 或本 bot app_id,退群失败" };
56
+ const args = ["im", "chat.members", "delete", "--chat-id", chatId, "--member-id-type", "app_id", "--data", JSON.stringify({ id_list: [selfApp] }), "--yes", "--json"];
57
+ if (options.dryRun)
58
+ args.push("--dry-run");
59
+ const result = options.larkJsonOut(args);
60
+ if (result?.ok === false)
61
+ return { ok: false, status: 400, error: options.feishuError(result) || "退群失败" };
62
+ return { ok: true, status: 200, data: { ok: true } };
63
+ }
64
+ if (/\/channels\/[^/]+$/.test(pathNoQuery) && method === "PATCH") {
65
+ const match = pathNoQuery.match(/\/channels\/([^/]+)$/);
66
+ const target = decodeURIComponent(match[1]);
67
+ const chatId = options.resolveChatId(target);
68
+ if (!chatId)
69
+ return { ok: false, status: 404, error: `未知频道 ${target}` };
70
+ const newName = body.name !== undefined ? String(body.name).replace(/^#/, "").slice(0, 60) : undefined;
71
+ const args = ["im", "+chat-update", "--chat-id", chatId, "--json"];
72
+ if (newName !== undefined)
73
+ args.push("--name", newName);
74
+ if (body.description !== undefined)
75
+ args.push("--description", String(body.description).slice(0, 100));
76
+ if (newName === undefined && body.description === undefined)
77
+ return { ok: false, status: 400, error: "飞书群仅支持改名称/描述;public/private 无法通过更新变更。" };
78
+ if (options.dryRun)
79
+ args.push("--dry-run");
80
+ const result = options.larkJsonOut(args);
81
+ if (!result || result.ok === false)
82
+ return { ok: false, status: 400, error: (result && options.feishuError(result)) || "改群失败" };
83
+ options.invalidateChatMeta(chatId);
84
+ const meta = options.chatMeta(chatId);
85
+ const name = newName !== undefined ? newName : ((meta && meta.name) || target.replace(/^#/, ""));
86
+ return { ok: true, status: 200, data: { name, type: body.visibility === "private" ? "private" : "public" } };
87
+ }
88
+ if (/\/attachments\/[^/]+$/.test(pathNoQuery) && method === "GET") {
89
+ return { ok: false, status: 501, error: "附件下载/预览暂未接入 larkin(当前仅支持上传并发送附件)。" };
90
+ }
91
+ if (requestPath.includes("/channel-members") || /\/channels\/[^/]+\/members/.test(requestPath)) {
92
+ const match = requestPath.match(/\/channels\/([^/]+)\/members/);
93
+ const ref = match ? decodeURIComponent(match[1]) : (options.query(requestPath, "channel") || "");
94
+ const { chat_id: chatId, channel_type: channelType } = options.channelContext(ref);
95
+ let agents = [];
96
+ let humans = [];
97
+ if (chatId) {
98
+ const result = options.larkJsonOut(["im", "+chat-members-list", "--chat-id", chatId, "--member-id-type", "user_id", "--json"]);
99
+ if (result?.ok && result.data) {
100
+ agents = (result.data.bots || []).map((bot) => ({ name: bot.name || bot.app_id || "bot", status: "active", id: bot.app_id || bot.member_id || null }));
101
+ humans = (result.data.users || []).map((user) => ({ name: user.name || user.member_id || "user", description: null, id: user.member_id || null }));
102
+ }
103
+ else if (result?.ok === false) {
104
+ const error = options.feishuError(result);
105
+ if (error) {
106
+ options.log(`members 报错→原样抛给 agent: ${error.slice(0, 120)}`);
107
+ return { ok: false, status: result.error?.code === 99991672 ? 403 : 400, error };
108
+ }
109
+ options.log(`members 降级(空): ${JSON.stringify(result.error || {}).slice(0, 120)}`);
110
+ }
111
+ }
112
+ return { ok: true, status: 200, data: { channel: { ref, type: channelType }, agents, humans } };
113
+ }
114
+ if (requestPath.includes("/channel")) {
115
+ const match = requestPath.match(/\/channels\/([^/?]+)/);
116
+ const ref = match ? decodeURIComponent(match[1]) : (options.query(requestPath, "channel") || options.query(requestPath, "ref") || "");
117
+ if (ref) {
118
+ const { label, chatId, meta, ctx, topicShort } = options.channelLabel(ref);
119
+ const isThread = Boolean(topicShort);
120
+ const channel = {
121
+ id: ref, name: label, joined: true, chatId: chatId || null,
122
+ chatMode: (meta && meta.chat_mode) || null, chatType: (meta && meta.chat_type) || null,
123
+ type: isThread ? "thread" : (String(ref).startsWith("dm:") ? "dm" : "channel"),
124
+ threadId: isThread ? ((ctx && ctx.thread_id) || null) : null,
125
+ };
126
+ return { ok: true, status: 200, data: { channel, channels: [channel] } };
127
+ }
128
+ const map = options.mappedChannels();
129
+ const channels = Object.keys(map).slice(0, 20).map((target) => {
130
+ const meta = options.chatMeta(map[target]);
131
+ return { id: target, name: (meta && meta.name) || String(target).replace(/^#/, "").replace(/^dm:@/, ""), joined: true };
132
+ });
133
+ return { ok: true, status: 200, data: { channels } };
134
+ }
135
+ if (requestPath.includes("/profile")) {
136
+ const app = options.feishuAppInfo();
137
+ const botName = options.botDisplayName() || (app && app.name);
138
+ const name = botName || options.agentName;
139
+ return { ok: true, status: 200, data: {
140
+ kind: "agent", id: options.agentId, isSelf: true, name, displayName: name,
141
+ description: (app && app.description) || null, avatarUrl: (app && app.avatarUrl) || null,
142
+ status: "active", serverRole: null, runtime: options.agentRuntime, model: options.agentModel,
143
+ reasoningEffort: options.agentEffort, executionMode: "byoc", computerId: null,
144
+ computerName: options.agentName, computerHostname: options.agentName, daemonVersion: null,
145
+ creator: null, createdAgents: [], createdAt: options.selectedAgent.createdAt ?? options.unknownCreatedAt, deletedAt: null,
146
+ } };
147
+ }
148
+ return undefined;
149
+ };
150
+ return { handle };
151
+ }
@@ -0,0 +1,148 @@
1
+ export function createChannelTransport(options) {
2
+ const handle = ({ path: requestPath, pathNoQuery, method, body }) => {
3
+ if (/\/channels$/.test(pathNoQuery) && method === "POST") {
4
+ const name = String(body.name || "").replace(/^#/, "").trim();
5
+ if (!name)
6
+ return { ok: false, status: 400, error: "建群需要 name" };
7
+ const allowPublic = process.env.LARKIN_ALLOW_PUBLIC_CHAT === "1";
8
+ const privateChat = !(allowPublic && body.visibility === "public");
9
+ const args = ["im", "+chat-create", "--name", name, "--chat-mode", "group", "--type", privateChat ? "private" : "public", "--json"];
10
+ if (body.description)
11
+ args.push("--description", String(body.description).slice(0, 100));
12
+ if (options.dryRun)
13
+ args.push("--dry-run");
14
+ const result = options.larkJsonOut(args);
15
+ if (!result || result.ok === false)
16
+ return { ok: false, status: 400, error: (result && options.feishuError(result)) || "建群失败" };
17
+ const chatId = (result.data && (result.data.chat_id || result.data.chatId)) || "";
18
+ if (chatId)
19
+ options.rememberChannel(`#${name}`, chatId, { name, chat_mode: "group", chat_type: "group" });
20
+ return { ok: true, status: 200, data: { id: `#${name}`, name, type: privateChat ? "private" : "public", joined: true, chatId: chatId || null } };
21
+ }
22
+ if (/\/channels\/[^/]+\/members$/.test(pathNoQuery) && (method === "POST" || method === "DELETE")) {
23
+ const match = pathNoQuery.match(/\/channels\/([^/]+)\/members$/);
24
+ const target = decodeURIComponent(match[1]);
25
+ const chatId = options.resolveChatId(target);
26
+ if (!chatId)
27
+ return { ok: false, status: 404, error: `未知频道 ${target}` };
28
+ const raw = body.user || body.agent || "";
29
+ const resolved = options.resolveMemberTo(chatId, raw);
30
+ if (!resolved)
31
+ return { ok: false, status: 400, error: `无法识别成员 "${raw}":请用 user_id(人) 或 app_id(cli_ 开头,bot);也支持 open_id(ou_)/union_id(on_)。可先跑 larkin channel members 查看各成员 id。` };
32
+ const verb = method === "DELETE" ? "delete" : "create";
33
+ const args = ["im", "chat.members", verb, "--chat-id", chatId, "--member-id-type", resolved.type, "--data", JSON.stringify({ id_list: resolved.ids }), "--json"];
34
+ if (verb === "delete")
35
+ args.push("--yes");
36
+ if (options.dryRun)
37
+ args.push("--dry-run");
38
+ const result = options.larkJsonOut(args);
39
+ if (result?.ok === false)
40
+ return { ok: false, status: 400, error: options.feishuError(result) || "成员操作失败" };
41
+ return { ok: true, status: 200, data: { member: { name: String(raw).replace(/^@/, "") || resolved.ids[0] } } };
42
+ }
43
+ if (/\/channels\/[^/]+\/join$/.test(pathNoQuery) && method === "POST") {
44
+ return { ok: false, status: 400, error: "飞书 bot 无法主动加入群聊;请让群主/管理员在群设置里邀请本 bot 进群。" };
45
+ }
46
+ if (/\/channels\/[^/]+\/leave$/.test(pathNoQuery) && method === "POST") {
47
+ const match = pathNoQuery.match(/\/channels\/([^/]+)\/leave$/);
48
+ const target = decodeURIComponent(match[1]);
49
+ const chatId = options.resolveChatId(target);
50
+ const selfApp = options.selectedAgent.feishuAppId;
51
+ if (!chatId || !selfApp)
52
+ return { ok: false, status: 400, error: "无法确定 chat_id 或本 bot app_id,退群失败" };
53
+ const args = ["im", "chat.members", "delete", "--chat-id", chatId, "--member-id-type", "app_id", "--data", JSON.stringify({ id_list: [selfApp] }), "--yes", "--json"];
54
+ if (options.dryRun)
55
+ args.push("--dry-run");
56
+ const result = options.larkJsonOut(args);
57
+ if (result?.ok === false)
58
+ return { ok: false, status: 400, error: options.feishuError(result) || "退群失败" };
59
+ return { ok: true, status: 200, data: { ok: true } };
60
+ }
61
+ if (/\/channels\/[^/]+$/.test(pathNoQuery) && method === "PATCH") {
62
+ const match = pathNoQuery.match(/\/channels\/([^/]+)$/);
63
+ const target = decodeURIComponent(match[1]);
64
+ const chatId = options.resolveChatId(target);
65
+ if (!chatId)
66
+ return { ok: false, status: 404, error: `未知频道 ${target}` };
67
+ const newName = body.name !== undefined ? String(body.name).replace(/^#/, "").slice(0, 60) : undefined;
68
+ const args = ["im", "+chat-update", "--chat-id", chatId, "--json"];
69
+ if (newName !== undefined)
70
+ args.push("--name", newName);
71
+ if (body.description !== undefined)
72
+ args.push("--description", String(body.description).slice(0, 100));
73
+ if (newName === undefined && body.description === undefined)
74
+ return { ok: false, status: 400, error: "飞书群仅支持改名称/描述;public/private 无法通过更新变更。" };
75
+ if (options.dryRun)
76
+ args.push("--dry-run");
77
+ const result = options.larkJsonOut(args);
78
+ if (!result || result.ok === false)
79
+ return { ok: false, status: 400, error: (result && options.feishuError(result)) || "改群失败" };
80
+ options.invalidateChatMeta(chatId);
81
+ const meta = options.chatMeta(chatId);
82
+ const name = newName !== undefined ? newName : ((meta && meta.name) || target.replace(/^#/, ""));
83
+ return { ok: true, status: 200, data: { name, type: body.visibility === "private" ? "private" : "public" } };
84
+ }
85
+ if (/\/attachments\/[^/]+$/.test(pathNoQuery) && method === "GET") {
86
+ return { ok: false, status: 501, error: "附件下载/预览暂未接入 larkin(当前仅支持上传并发送附件)。" };
87
+ }
88
+ if (requestPath.includes("/channel-members") || /\/channels\/[^/]+\/members/.test(requestPath)) {
89
+ const match = requestPath.match(/\/channels\/([^/]+)\/members/);
90
+ const ref = match ? decodeURIComponent(match[1]) : (options.query(requestPath, "channel") || "");
91
+ const { chat_id: chatId, channel_type: channelType } = options.channelContext(ref);
92
+ let agents = [];
93
+ let humans = [];
94
+ if (chatId) {
95
+ const result = options.larkJsonOut(["im", "+chat-members-list", "--chat-id", chatId, "--member-id-type", "user_id", "--json"]);
96
+ if (result?.ok && result.data) {
97
+ agents = (result.data.bots || []).map((bot) => ({ name: bot.name || bot.app_id || "bot", status: "active", id: bot.app_id || bot.member_id || null }));
98
+ humans = (result.data.users || []).map((user) => ({ name: user.name || user.member_id || "user", description: null, id: user.member_id || null }));
99
+ }
100
+ else if (result?.ok === false) {
101
+ const error = options.feishuError(result);
102
+ if (error) {
103
+ options.log(`members 报错→原样抛给 agent: ${error.slice(0, 120)}`);
104
+ return { ok: false, status: result.error?.code === 99991672 ? 403 : 400, error };
105
+ }
106
+ options.log(`members 降级(空): ${JSON.stringify(result.error || {}).slice(0, 120)}`);
107
+ }
108
+ }
109
+ return { ok: true, status: 200, data: { channel: { ref, type: channelType }, agents, humans } };
110
+ }
111
+ if (requestPath.includes("/channel")) {
112
+ const match = requestPath.match(/\/channels\/([^/?]+)/);
113
+ const ref = match ? decodeURIComponent(match[1]) : (options.query(requestPath, "channel") || options.query(requestPath, "ref") || "");
114
+ if (ref) {
115
+ const { label, chatId, meta, ctx, topicShort } = options.channelLabel(ref);
116
+ const isThread = Boolean(topicShort);
117
+ const channel = {
118
+ id: ref, name: label, joined: true, chatId: chatId || null,
119
+ chatMode: (meta && meta.chat_mode) || null, chatType: (meta && meta.chat_type) || null,
120
+ type: isThread ? "thread" : (String(ref).startsWith("dm:") ? "dm" : "channel"),
121
+ threadId: isThread ? ((ctx && ctx.thread_id) || null) : null,
122
+ };
123
+ return { ok: true, status: 200, data: { channel, channels: [channel] } };
124
+ }
125
+ const map = options.mappedChannels();
126
+ const channels = Object.keys(map).slice(0, 20).map((target) => {
127
+ const meta = options.chatMeta(map[target]);
128
+ return { id: target, name: (meta && meta.name) || String(target).replace(/^#/, "").replace(/^dm:@/, ""), joined: true };
129
+ });
130
+ return { ok: true, status: 200, data: { channels } };
131
+ }
132
+ if (requestPath.includes("/profile")) {
133
+ const app = options.feishuAppInfo();
134
+ const botName = options.botDisplayName() || (app && app.name);
135
+ const name = botName || options.agentName;
136
+ return { ok: true, status: 200, data: {
137
+ kind: "agent", id: options.agentId, isSelf: true, name, displayName: name,
138
+ description: (app && app.description) || null, avatarUrl: (app && app.avatarUrl) || null,
139
+ status: "active", serverRole: null, runtime: options.agentRuntime, model: options.agentModel,
140
+ reasoningEffort: options.agentEffort, executionMode: "byoc", computerId: null,
141
+ computerName: options.agentName, computerHostname: options.agentName, daemonVersion: null,
142
+ creator: null, createdAgents: [], createdAt: options.selectedAgent.createdAt ?? options.unknownCreatedAt, deletedAt: null,
143
+ } };
144
+ }
145
+ return undefined;
146
+ };
147
+ return { handle };
148
+ }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DOCUMENT_COMMENT_EVENT = void 0;
4
+ exports.documentCommentEventId = documentCommentEventId;
5
+ exports.documentCommentNoticeType = documentCommentNoticeType;
6
+ exports.documentCommentMessageId = documentCommentMessageId;
7
+ exports.documentCommentTarget = documentCommentTarget;
8
+ exports.parseDocumentCommentTarget = parseDocumentCommentTarget;
9
+ exports.extractDocumentCommentContent = extractDocumentCommentContent;
10
+ exports.resolveDocumentCommentContext = resolveDocumentCommentContext;
11
+ exports.projectDocumentCommentEnvelope = projectDocumentCommentEnvelope;
12
+ const node_crypto_1 = require("node:crypto");
13
+ exports.DOCUMENT_COMMENT_EVENT = "drive.notice.comment_add_v1";
14
+ function record(value) {
15
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
16
+ }
17
+ function rawEvent(event) {
18
+ return record(event.raw);
19
+ }
20
+ function documentCommentEventId(event) {
21
+ const raw = rawEvent(event);
22
+ const header = record(raw?.header);
23
+ const value = raw?.event_id ?? header?.event_id;
24
+ return typeof value === "string" && value ? value : null;
25
+ }
26
+ function documentCommentNoticeType(event) {
27
+ const raw = rawEvent(event);
28
+ const meta = record(raw?.notice_meta);
29
+ const value = raw?.notice_type ?? meta?.notice_type;
30
+ return typeof value === "string" && value ? value : event.replyId ? "add_reply" : "add_comment";
31
+ }
32
+ function documentCommentMessageId(agentId, event) {
33
+ // Feishu may redeliver the same semantic notice under a different event_id.
34
+ // The comment locator is the durable identity; event_id remains transport metadata only.
35
+ const stable = [agentId, event.fileType, event.fileToken, event.commentId, event.replyId || "", documentCommentNoticeType(event)];
36
+ return `doc_comment_${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify(stable)).digest("hex").slice(0, 32)}`;
37
+ }
38
+ function documentCommentTarget(target, commentId, topLevel = false) {
39
+ return `document-comment:${target.fileType}:${target.fileToken}:${commentId}:${topLevel ? "top-level" : "in-thread"}`;
40
+ }
41
+ function parseDocumentCommentTarget(value) {
42
+ const match = /^document-comment:(doc|docx|sheet|file):([A-Za-z0-9_-]+):([A-Za-z0-9_-]+):(in-thread|top-level)$/.exec(value);
43
+ return match ? {
44
+ fileType: match[1], fileToken: match[2], commentId: match[3], topLevel: match[4] === "top-level",
45
+ } : null;
46
+ }
47
+ function extractDocumentCommentContent(fetched, replyId) {
48
+ const reply = replyId
49
+ ? fetched.replies.find((candidate) => candidate.reply_id === replyId)
50
+ : fetched.replies.at(-1);
51
+ if (!reply?.reply_id)
52
+ return null;
53
+ const content = (reply.content?.elements ?? []).map((element) => {
54
+ if (element.type === "text_run")
55
+ return element.text_run?.text ?? "";
56
+ if (element.type === "docs_link")
57
+ return element.docs_link?.url ?? "";
58
+ return "";
59
+ }).join("").trim();
60
+ return content ? { content, targetReplyId: reply.reply_id } : null;
61
+ }
62
+ async function resolveDocumentCommentContext(comments, event) {
63
+ const target = await comments.resolveTarget(event.fileToken, event.fileType);
64
+ if (!target)
65
+ return null;
66
+ const fetched = await comments.fetch(target, event.commentId);
67
+ if (!fetched)
68
+ return null;
69
+ const extracted = extractDocumentCommentContent(fetched, event.replyId);
70
+ if (!extracted)
71
+ return null;
72
+ return {
73
+ target,
74
+ content: extracted.content,
75
+ quote: fetched.quote?.trim() || null,
76
+ isWhole: fetched.isWhole,
77
+ targetReplyId: extracted.targetReplyId,
78
+ noticeType: documentCommentNoticeType(event),
79
+ };
80
+ }
81
+ function projectDocumentCommentEnvelope(input) {
82
+ const { agentId, event, context, subscription } = input;
83
+ const messageId = documentCommentMessageId(agentId, event);
84
+ const timestamp = Number.isFinite(event.timestamp) ? new Date(event.timestamp).toISOString() : new Date().toISOString();
85
+ const target = documentCommentTarget(context.target, event.commentId, context.isWhole);
86
+ return {
87
+ kind: "document_comment",
88
+ event_type: exports.DOCUMENT_COMMENT_EVENT,
89
+ message_id: messageId,
90
+ seq: Number.isSafeInteger(event.timestamp) && event.timestamp > 0 ? event.timestamp : 1,
91
+ target,
92
+ wake: true,
93
+ comment_subscription_mode: subscription.mode,
94
+ comment_subscription_status: subscription.status,
95
+ comment_subscription_source: subscription.source,
96
+ comment_subscription_dimension: subscription.dimension,
97
+ mentioned_bot: subscription.mentionedBot,
98
+ timestamp,
99
+ sender_id: event.operator.openId,
100
+ sender_name: event.operator.openId,
101
+ sender_type: "human",
102
+ content: context.content,
103
+ quote: context.quote,
104
+ notice_type: context.noticeType,
105
+ file_type: context.target.fileType,
106
+ file_token: context.target.fileToken,
107
+ source_file_type: event.fileType,
108
+ source_file_token: event.fileToken,
109
+ comment_id: event.commentId,
110
+ reply_id: event.replyId ?? null,
111
+ target_reply_id: context.targetReplyId,
112
+ is_whole: context.isWhole,
113
+ reply: {
114
+ command: "comment reply",
115
+ argv: ["comment", "reply", "--message-id", messageId, "--text", "<reply_text>"],
116
+ mode: context.isWhole ? "top-level-fallback" : "in-thread",
117
+ target,
118
+ },
119
+ };
120
+ }
@@ -0,0 +1,109 @@
1
+ import { createHash } from "node:crypto";
2
+ export const DOCUMENT_COMMENT_EVENT = "drive.notice.comment_add_v1";
3
+ function record(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5
+ }
6
+ function rawEvent(event) {
7
+ return record(event.raw);
8
+ }
9
+ export function documentCommentEventId(event) {
10
+ const raw = rawEvent(event);
11
+ const header = record(raw?.header);
12
+ const value = raw?.event_id ?? header?.event_id;
13
+ return typeof value === "string" && value ? value : null;
14
+ }
15
+ export function documentCommentNoticeType(event) {
16
+ const raw = rawEvent(event);
17
+ const meta = record(raw?.notice_meta);
18
+ const value = raw?.notice_type ?? meta?.notice_type;
19
+ return typeof value === "string" && value ? value : event.replyId ? "add_reply" : "add_comment";
20
+ }
21
+ export function documentCommentMessageId(agentId, event) {
22
+ // Feishu may redeliver the same semantic notice under a different event_id.
23
+ // The comment locator is the durable identity; event_id remains transport metadata only.
24
+ const stable = [agentId, event.fileType, event.fileToken, event.commentId, event.replyId || "", documentCommentNoticeType(event)];
25
+ return `doc_comment_${createHash("sha256").update(JSON.stringify(stable)).digest("hex").slice(0, 32)}`;
26
+ }
27
+ export function documentCommentTarget(target, commentId, topLevel = false) {
28
+ return `document-comment:${target.fileType}:${target.fileToken}:${commentId}:${topLevel ? "top-level" : "in-thread"}`;
29
+ }
30
+ export function parseDocumentCommentTarget(value) {
31
+ const match = /^document-comment:(doc|docx|sheet|file):([A-Za-z0-9_-]+):([A-Za-z0-9_-]+):(in-thread|top-level)$/.exec(value);
32
+ return match ? {
33
+ fileType: match[1], fileToken: match[2], commentId: match[3], topLevel: match[4] === "top-level",
34
+ } : null;
35
+ }
36
+ export function extractDocumentCommentContent(fetched, replyId) {
37
+ const reply = replyId
38
+ ? fetched.replies.find((candidate) => candidate.reply_id === replyId)
39
+ : fetched.replies.at(-1);
40
+ if (!reply?.reply_id)
41
+ return null;
42
+ const content = (reply.content?.elements ?? []).map((element) => {
43
+ if (element.type === "text_run")
44
+ return element.text_run?.text ?? "";
45
+ if (element.type === "docs_link")
46
+ return element.docs_link?.url ?? "";
47
+ return "";
48
+ }).join("").trim();
49
+ return content ? { content, targetReplyId: reply.reply_id } : null;
50
+ }
51
+ export async function resolveDocumentCommentContext(comments, event) {
52
+ const target = await comments.resolveTarget(event.fileToken, event.fileType);
53
+ if (!target)
54
+ return null;
55
+ const fetched = await comments.fetch(target, event.commentId);
56
+ if (!fetched)
57
+ return null;
58
+ const extracted = extractDocumentCommentContent(fetched, event.replyId);
59
+ if (!extracted)
60
+ return null;
61
+ return {
62
+ target,
63
+ content: extracted.content,
64
+ quote: fetched.quote?.trim() || null,
65
+ isWhole: fetched.isWhole,
66
+ targetReplyId: extracted.targetReplyId,
67
+ noticeType: documentCommentNoticeType(event),
68
+ };
69
+ }
70
+ export function projectDocumentCommentEnvelope(input) {
71
+ const { agentId, event, context, subscription } = input;
72
+ const messageId = documentCommentMessageId(agentId, event);
73
+ const timestamp = Number.isFinite(event.timestamp) ? new Date(event.timestamp).toISOString() : new Date().toISOString();
74
+ const target = documentCommentTarget(context.target, event.commentId, context.isWhole);
75
+ return {
76
+ kind: "document_comment",
77
+ event_type: DOCUMENT_COMMENT_EVENT,
78
+ message_id: messageId,
79
+ seq: Number.isSafeInteger(event.timestamp) && event.timestamp > 0 ? event.timestamp : 1,
80
+ target,
81
+ wake: true,
82
+ comment_subscription_mode: subscription.mode,
83
+ comment_subscription_status: subscription.status,
84
+ comment_subscription_source: subscription.source,
85
+ comment_subscription_dimension: subscription.dimension,
86
+ mentioned_bot: subscription.mentionedBot,
87
+ timestamp,
88
+ sender_id: event.operator.openId,
89
+ sender_name: event.operator.openId,
90
+ sender_type: "human",
91
+ content: context.content,
92
+ quote: context.quote,
93
+ notice_type: context.noticeType,
94
+ file_type: context.target.fileType,
95
+ file_token: context.target.fileToken,
96
+ source_file_type: event.fileType,
97
+ source_file_token: event.fileToken,
98
+ comment_id: event.commentId,
99
+ reply_id: event.replyId ?? null,
100
+ target_reply_id: context.targetReplyId,
101
+ is_whole: context.isWhole,
102
+ reply: {
103
+ command: "comment reply",
104
+ argv: ["comment", "reply", "--message-id", messageId, "--text", "<reply_text>"],
105
+ mode: context.isWhole ? "top-level-fallback" : "in-thread",
106
+ target,
107
+ },
108
+ };
109
+ }