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.
- package/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createAgentStateStore } from "../agent/agent-state-store.mjs";
|
|
7
|
+
import { evaluateFreshness } from "../agent/freshness-gate.mjs";
|
|
8
|
+
import { feishuImFreshnessAdapter, feishuImTarget, mergeFeishuImCursor, serializeFeishuImTarget, } from "../feishu/im-freshness-adapter.mjs";
|
|
9
|
+
import * as larkinConfig from "../platform/config.mjs";
|
|
10
|
+
import { resolveOfficialLarkCli } from "./official-lark-cli.mjs";
|
|
11
|
+
import { assertAgentWorkspaceBound, managedLarkCliEnv } from "./agent-lark-cli-workspace.mjs";
|
|
12
|
+
import { parseDocumentCommentTarget } from "../feishu/document-comment.mjs";
|
|
13
|
+
import { SpanKind } from "@opentelemetry/api";
|
|
14
|
+
import { loadTelemetryConfig } from "../platform/telemetry-config.mjs";
|
|
15
|
+
import { telemetrySingleton } from "../platform/telemetry-tracing.mjs";
|
|
16
|
+
import { packageVersion } from "../platform/build-info.mjs";
|
|
17
|
+
function portableSignalCode(signal) {
|
|
18
|
+
if (signal === "SIGINT")
|
|
19
|
+
return 130;
|
|
20
|
+
if (signal === "SIGTERM")
|
|
21
|
+
return 143;
|
|
22
|
+
if (signal === "SIGKILL")
|
|
23
|
+
return 137;
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
const HELP_FLAGS = new Set(["--help", "-h"]);
|
|
27
|
+
const MANAGEMENT_COMMANDS = new Set(["auth", "config", "profile", "update", "install"]);
|
|
28
|
+
const USER_ONLY_COMMANDS = new Set(["attendance", "mail", "okr"]);
|
|
29
|
+
const POLICY_VALUE_FLAGS = new Set([
|
|
30
|
+
"--as", "--profile", "--config-dir", "--agent", "--chat-id", "--user-id", "--message-id", "--idempotency-key", "--thread-id",
|
|
31
|
+
]);
|
|
32
|
+
const PROTECTED_VALUE_FLAGS = new Set([
|
|
33
|
+
...POLICY_VALUE_FLAGS,
|
|
34
|
+
"--text", "--markdown", "--content", "--image", "--file", "--video", "--video-cover", "--audio", "--msg-type",
|
|
35
|
+
"--format", "--jq", "-q", "--output", "-o", "--data", "--params", "--receive-id-type", "--thread-id", "--uuid",
|
|
36
|
+
"--page-delay", "--page-limit", "--page-size",
|
|
37
|
+
]);
|
|
38
|
+
const RAW_IM_WRITE_OPERATIONS = new Set([
|
|
39
|
+
"create", "reply", "patch", "update", "forward", "merge_forward", "delete", "urgent_app", "urgent_phone", "urgent_sms",
|
|
40
|
+
]);
|
|
41
|
+
const HISTORY_VALUE_FLAGS = new Set([
|
|
42
|
+
...POLICY_VALUE_FLAGS,
|
|
43
|
+
"--start", "--end", "--order", "--sort", "--page-size", "--page-token", "--thread",
|
|
44
|
+
"--format", "--jq", "-q", "--download-dir",
|
|
45
|
+
]);
|
|
46
|
+
function defaultIo() {
|
|
47
|
+
return { stdout: (text) => process.stdout.write(text), stderr: (text) => process.stderr.write(text) };
|
|
48
|
+
}
|
|
49
|
+
function exactPath(argv, pathSegments) {
|
|
50
|
+
return pathSegments.every((segment, index) => argv[index] === segment);
|
|
51
|
+
}
|
|
52
|
+
function nativeArgvBeforeBoundary(argv) {
|
|
53
|
+
const boundary = argv.indexOf("--");
|
|
54
|
+
return argv.slice(0, boundary < 0 ? argv.length : boundary);
|
|
55
|
+
}
|
|
56
|
+
function hasCanonicalUnprotectedCommandPath(argv) {
|
|
57
|
+
const nativeArgv = nativeArgvBeforeBoundary(argv);
|
|
58
|
+
const service = nativeArgv[0];
|
|
59
|
+
const command = nativeArgv[1];
|
|
60
|
+
if (!service || service.startsWith("-") || !command || command.startsWith("-"))
|
|
61
|
+
return false;
|
|
62
|
+
if (service !== "im")
|
|
63
|
+
return service !== "api" && service !== "larkin-draft";
|
|
64
|
+
if (command === "+messages-send" || command === "+messages-reply" || command === "api") {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (command.startsWith("+"))
|
|
68
|
+
return true;
|
|
69
|
+
const operation = nativeArgv[2];
|
|
70
|
+
if (!operation || operation.startsWith("-") || operation === "api" || operation === "larkin-draft")
|
|
71
|
+
return false;
|
|
72
|
+
if (command === "messages" && RAW_IM_WRITE_OPERATIONS.has(operation))
|
|
73
|
+
return false;
|
|
74
|
+
if (command === "threads" && (operation === "forward" || operation === "merge_forward"))
|
|
75
|
+
return false;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
function protectedSyntaxTokens(argv) {
|
|
79
|
+
const tokens = [];
|
|
80
|
+
const nativeArgv = nativeArgvBeforeBoundary(argv);
|
|
81
|
+
for (let index = 0; index < nativeArgv.length; index += 1) {
|
|
82
|
+
const argument = nativeArgv[index];
|
|
83
|
+
const inlineFlag = [...PROTECTED_VALUE_FLAGS].find((flag) => argument.startsWith(`${flag}=`));
|
|
84
|
+
if (inlineFlag) {
|
|
85
|
+
tokens.push(inlineFlag);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
tokens.push(argument);
|
|
89
|
+
if (PROTECTED_VALUE_FLAGS.has(argument) && index + 1 < nativeArgv.length)
|
|
90
|
+
index += 1;
|
|
91
|
+
}
|
|
92
|
+
return tokens;
|
|
93
|
+
}
|
|
94
|
+
function hasNativeHelpFlag(argv) {
|
|
95
|
+
return protectedSyntaxTokens(argv).some((argument) => HELP_FLAGS.has(argument));
|
|
96
|
+
}
|
|
97
|
+
function protectedOperations(argv) {
|
|
98
|
+
const tokens = protectedSyntaxTokens(argv);
|
|
99
|
+
const operations = [];
|
|
100
|
+
for (const token of tokens) {
|
|
101
|
+
if (token === "+messages-send")
|
|
102
|
+
operations.push("send");
|
|
103
|
+
else if (token === "+messages-reply")
|
|
104
|
+
operations.push("reply");
|
|
105
|
+
else if (token === "api")
|
|
106
|
+
operations.push("api");
|
|
107
|
+
}
|
|
108
|
+
const hasIm = tokens.includes("im");
|
|
109
|
+
if (hasIm && tokens.includes("messages")) {
|
|
110
|
+
for (const token of tokens) {
|
|
111
|
+
if (!RAW_IM_WRITE_OPERATIONS.has(token))
|
|
112
|
+
continue;
|
|
113
|
+
operations.push((token === "patch" || token === "update" ? `card-${token}` : `raw-${token}`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (hasIm && tokens.includes("threads")) {
|
|
117
|
+
for (const token of tokens) {
|
|
118
|
+
if (token === "forward" || token === "merge_forward")
|
|
119
|
+
operations.push(`thread-${token}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return operations;
|
|
123
|
+
}
|
|
124
|
+
function uniqueProtectedOperation(operations, expected) {
|
|
125
|
+
return operations.length === 1 && operations[0] === expected;
|
|
126
|
+
}
|
|
127
|
+
function noncanonicalProtectedDecision() {
|
|
128
|
+
return {
|
|
129
|
+
kind: "denied",
|
|
130
|
+
reason: "受保护的 write/API 命令路径不明确;请把 service/subcommand 放在前面,再按原生 --help 提示传入 flags",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function parsePolicyArgv(argv) {
|
|
134
|
+
const nativeArgv = nativeArgvBeforeBoundary(argv);
|
|
135
|
+
const help = hasNativeHelpFlag(argv);
|
|
136
|
+
// Native help is an observational path. It must reach Cobra byte-for-byte even
|
|
137
|
+
// when the surrounding argv would be rejected for a real operation.
|
|
138
|
+
if (help)
|
|
139
|
+
return { commandArgv: nativeArgv, flags: new Map(), help: true, error: null };
|
|
140
|
+
const commandArgv = [];
|
|
141
|
+
const flags = new Map();
|
|
142
|
+
for (let index = 0; index < nativeArgv.length; index += 1) {
|
|
143
|
+
const argument = nativeArgv[index];
|
|
144
|
+
const inlineFlag = [...POLICY_VALUE_FLAGS].find((flag) => argument.startsWith(`${flag}=`));
|
|
145
|
+
const flag = POLICY_VALUE_FLAGS.has(argument) ? argument : inlineFlag;
|
|
146
|
+
if (!flag) {
|
|
147
|
+
commandArgv.push(argument);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const value = inlineFlag ? argument.slice(flag.length + 1) : nativeArgv[index + 1];
|
|
151
|
+
if (!inlineFlag)
|
|
152
|
+
index += 1;
|
|
153
|
+
if (!value || value.startsWith("-"))
|
|
154
|
+
return { commandArgv, flags, help: false, error: `${flag} 缺少有效值` };
|
|
155
|
+
if (flags.has(flag))
|
|
156
|
+
return { commandArgv, flags, help: false, error: `${flag} 不允许重复或冲突赋值` };
|
|
157
|
+
flags.set(flag, value);
|
|
158
|
+
}
|
|
159
|
+
return { commandArgv, flags, help: false, error: null };
|
|
160
|
+
}
|
|
161
|
+
function policyFlagValue(argv, flag) {
|
|
162
|
+
return parsePolicyArgv(argv).flags.get(flag) ?? null;
|
|
163
|
+
}
|
|
164
|
+
export function classifyLarkCliCommand(argv) {
|
|
165
|
+
const parsed = parsePolicyArgv(argv);
|
|
166
|
+
if (parsed.help)
|
|
167
|
+
return { kind: "passthrough" };
|
|
168
|
+
if (parsed.error)
|
|
169
|
+
return { kind: "denied", reason: `参数边界:${parsed.error}` };
|
|
170
|
+
for (const flag of ["--profile", "--config-dir", "--agent"]) {
|
|
171
|
+
if (parsed.flags.has(flag))
|
|
172
|
+
return { kind: "denied", reason: `身份边界:${flag} 由 Larkin Runtime 锁定` };
|
|
173
|
+
}
|
|
174
|
+
const as = parsed.flags.get("--as");
|
|
175
|
+
if (as && as !== "bot")
|
|
176
|
+
return { kind: "denied", reason: "身份边界:Runtime 内 lark-cli 只允许 Bot identity" };
|
|
177
|
+
const command = parsed.commandArgv[0] || "";
|
|
178
|
+
if (command === "comment")
|
|
179
|
+
return exactPath(parsed.commandArgv, ["comment", "reply"])
|
|
180
|
+
? { kind: "comment-reply" }
|
|
181
|
+
: { kind: "denied", reason: "comment 只支持绑定 canonical Inbox locator 的 `comment reply`" };
|
|
182
|
+
if (MANAGEMENT_COMMANDS.has(command))
|
|
183
|
+
return { kind: "denied", reason: `身份边界:Runtime 不开放 lark-cli ${command} 管理命令` };
|
|
184
|
+
if (command === "event")
|
|
185
|
+
return { kind: "denied", reason: "Runtime 不允许另开 event 连接与 Host 争抢事件流" };
|
|
186
|
+
if (USER_ONLY_COMMANDS.has(command))
|
|
187
|
+
return { kind: "denied", reason: `${command} 是 user-only identity 域` };
|
|
188
|
+
if (hasCanonicalUnprotectedCommandPath(argv))
|
|
189
|
+
return { kind: "passthrough" };
|
|
190
|
+
const protectedPaths = protectedOperations(argv);
|
|
191
|
+
if (command === "larkin-draft")
|
|
192
|
+
return { kind: "denied", reason: "larkin-draft 已移除;freshness conflict 后请重新判断并执行普通写命令" };
|
|
193
|
+
if (exactPath(parsed.commandArgv, ["im", "+messages-send"]))
|
|
194
|
+
return uniqueProtectedOperation(protectedPaths, "send")
|
|
195
|
+
? (parsed.flags.has("--thread-id")
|
|
196
|
+
? { kind: "denied", reason: "+messages-send 不支持 --thread-id;线程内写入请使用 +messages-reply --message-id ... --reply-in-thread" }
|
|
197
|
+
: parsed.commandArgv.includes("--dry-run") ? { kind: "passthrough" } : { kind: "guarded", operation: "send" })
|
|
198
|
+
: noncanonicalProtectedDecision();
|
|
199
|
+
if (exactPath(parsed.commandArgv, ["im", "+messages-reply"]))
|
|
200
|
+
return uniqueProtectedOperation(protectedPaths, "reply")
|
|
201
|
+
? (parsed.commandArgv.includes("--dry-run") ? { kind: "passthrough" } : { kind: "guarded", operation: "reply" })
|
|
202
|
+
: noncanonicalProtectedDecision();
|
|
203
|
+
if (exactPath(parsed.commandArgv, ["im", "messages", "patch"]) || exactPath(parsed.commandArgv, ["im", "messages", "update"])) {
|
|
204
|
+
const expected = parsed.commandArgv[2] === "patch" ? "card-patch" : "card-update";
|
|
205
|
+
return uniqueProtectedOperation(protectedPaths, expected)
|
|
206
|
+
? (parsed.commandArgv.includes("--dry-run") ? { kind: "passthrough" } : { kind: "guarded", operation: "card" })
|
|
207
|
+
: noncanonicalProtectedDecision();
|
|
208
|
+
}
|
|
209
|
+
if (exactPath(parsed.commandArgv, ["im", "messages", "create"]) || exactPath(parsed.commandArgv, ["im", "messages", "reply"])) {
|
|
210
|
+
const expected = parsed.commandArgv[2] === "create" ? "raw-create" : "raw-reply";
|
|
211
|
+
return uniqueProtectedOperation(protectedPaths, expected)
|
|
212
|
+
? { kind: "denied", reason: "该原始 IM 写入口会旁路 target freshness;请使用 +messages-send/+messages-reply" }
|
|
213
|
+
: noncanonicalProtectedDecision();
|
|
214
|
+
}
|
|
215
|
+
if (["forward", "merge_forward", "delete", "urgent_app", "urgent_phone", "urgent_sms"]
|
|
216
|
+
.some((operation) => exactPath(parsed.commandArgv, ["im", "messages", operation]))) {
|
|
217
|
+
const expected = `raw-${parsed.commandArgv[2]}`;
|
|
218
|
+
return uniqueProtectedOperation(protectedPaths, expected)
|
|
219
|
+
? { kind: "denied", reason: "该 IM 写入口无法建立 target freshness;请先用 larkin inbox poll 读取目标,再使用受保护的 +messages-send/+messages-reply" }
|
|
220
|
+
: noncanonicalProtectedDecision();
|
|
221
|
+
}
|
|
222
|
+
if (["forward", "merge_forward"]
|
|
223
|
+
.some((operation) => exactPath(parsed.commandArgv, ["im", "threads", operation]))) {
|
|
224
|
+
const expected = `thread-${parsed.commandArgv[2]}`;
|
|
225
|
+
return uniqueProtectedOperation(protectedPaths, expected)
|
|
226
|
+
? { kind: "denied", reason: "该 IM forwarding 入口无法建立 target freshness;请先用 larkin inbox poll 读取目标,再使用受保护的 +messages-send/+messages-reply" }
|
|
227
|
+
: noncanonicalProtectedDecision();
|
|
228
|
+
}
|
|
229
|
+
if (command === "api")
|
|
230
|
+
return uniqueProtectedOperation(protectedPaths, "api")
|
|
231
|
+
? { kind: "denied", reason: "generic API 会旁路 Runtime identity/freshness policy" }
|
|
232
|
+
: noncanonicalProtectedDecision();
|
|
233
|
+
if (protectedPaths.length > 0)
|
|
234
|
+
return noncanonicalProtectedDecision();
|
|
235
|
+
return { kind: "passthrough" };
|
|
236
|
+
}
|
|
237
|
+
function parseCommentReply(argv) {
|
|
238
|
+
const values = new Map();
|
|
239
|
+
const positionals = [];
|
|
240
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
241
|
+
const argument = argv[index];
|
|
242
|
+
if (argument === "--json")
|
|
243
|
+
continue;
|
|
244
|
+
const inline = ["--message-id", "--text"].find((flag) => argument.startsWith(`${flag}=`));
|
|
245
|
+
const flag = inline ?? (["--message-id", "--text"].includes(argument) ? argument : null);
|
|
246
|
+
if (!flag) {
|
|
247
|
+
if (argument.startsWith("-"))
|
|
248
|
+
throw new Error(`comment reply 不支持参数 ${argument}`);
|
|
249
|
+
positionals.push(argument);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (values.has(flag))
|
|
253
|
+
throw new Error(`${flag} 只能指定一次`);
|
|
254
|
+
const value = inline ? argument.slice(flag.length + 1) : argv[++index];
|
|
255
|
+
if (value === undefined)
|
|
256
|
+
throw new Error(`${flag} 需要值`);
|
|
257
|
+
values.set(flag, value);
|
|
258
|
+
}
|
|
259
|
+
if (positionals.length !== 2 || positionals[0] !== "comment" || positionals[1] !== "reply") {
|
|
260
|
+
throw new Error("用法: larkin comment reply --message-id <doc_comment_id> --text '<reply>' --json");
|
|
261
|
+
}
|
|
262
|
+
const messageId = values.get("--message-id") || "";
|
|
263
|
+
const text = values.get("--text") || "";
|
|
264
|
+
if (!/^doc_comment_[0-9a-f]{32}$/.test(messageId))
|
|
265
|
+
throw new Error("comment reply 需要 Inbox 提供的 doc_comment message id");
|
|
266
|
+
if (!text.trim())
|
|
267
|
+
throw new Error("comment reply 的 --text 不能为空");
|
|
268
|
+
if (text.length > 20_000)
|
|
269
|
+
throw new Error("comment reply 的 --text 超过 20000 字符");
|
|
270
|
+
return { messageId, text };
|
|
271
|
+
}
|
|
272
|
+
function runCommentReply(argv, privateEnv, io, dependencies, store) {
|
|
273
|
+
const input = parseCommentReply(argv);
|
|
274
|
+
const targetKey = store.resolveInboxMessageTarget(input.messageId);
|
|
275
|
+
const target = targetKey ? parseDocumentCommentTarget(targetKey) : null;
|
|
276
|
+
if (!target)
|
|
277
|
+
throw new Error("comment reply 无法从当前 Agent Inbox 绑定文档评论 locator;先 poll 该消息且不得跨 Agent/评论回复");
|
|
278
|
+
if (!store.inboxTargetIsFresh(targetKey))
|
|
279
|
+
throw new Error("comment reply 需要先 poll 当前 document-comment target 的最新 Inbox 消息");
|
|
280
|
+
const digest = createHash("sha256").update(input.text).digest("hex");
|
|
281
|
+
const claim = store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
282
|
+
state.document_comment_replies ??= {};
|
|
283
|
+
const prior = state.document_comment_replies[input.messageId];
|
|
284
|
+
if (prior?.status === "sent" && prior.digest === digest)
|
|
285
|
+
return "sent";
|
|
286
|
+
if (prior?.status === "sending" && prior.digest === digest)
|
|
287
|
+
return "ambiguous";
|
|
288
|
+
if (prior && prior.status !== "failed" && prior.digest !== digest)
|
|
289
|
+
return "conflict";
|
|
290
|
+
state.document_comment_replies[input.messageId] = { digest, status: "sending", updated_at: new Date().toISOString() };
|
|
291
|
+
const keys = Object.keys(state.document_comment_replies);
|
|
292
|
+
for (const stale of keys.slice(0, Math.max(0, keys.length - 512)))
|
|
293
|
+
delete state.document_comment_replies[stale];
|
|
294
|
+
return "ready";
|
|
295
|
+
});
|
|
296
|
+
if (claim === "sent") {
|
|
297
|
+
io.stdout(`${JSON.stringify({ ok: true, identity: "bot", committed: true, duplicate: true, target: targetKey })}\n`);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
if (claim === "ambiguous")
|
|
301
|
+
throw new Error("comment reply 上次调用结果不明确,已 fail-closed 以避免重复评论;请由用户检查原评论线程");
|
|
302
|
+
if (claim === "conflict")
|
|
303
|
+
throw new Error("comment reply 已为同一 Inbox 消息提交不同正文,拒绝覆盖或重复发送");
|
|
304
|
+
const nativeArgs = target.topLevel
|
|
305
|
+
? [
|
|
306
|
+
"drive", "file.comments", "create_v2",
|
|
307
|
+
"--file-token", target.fileToken,
|
|
308
|
+
"--data", JSON.stringify({
|
|
309
|
+
file_type: target.fileType,
|
|
310
|
+
reply_elements: [{ type: "text", text: input.text }],
|
|
311
|
+
}),
|
|
312
|
+
"--as", "bot",
|
|
313
|
+
]
|
|
314
|
+
: [
|
|
315
|
+
"drive", "file.comment.replys", "create",
|
|
316
|
+
"--file-token", target.fileToken,
|
|
317
|
+
"--comment-id", target.commentId,
|
|
318
|
+
"--file-type", target.fileType,
|
|
319
|
+
"--data", JSON.stringify({
|
|
320
|
+
content: { elements: [{ type: "text_run", text_run: { text: input.text } }] },
|
|
321
|
+
}),
|
|
322
|
+
"--as", "bot",
|
|
323
|
+
];
|
|
324
|
+
const result = callNative(nativeArgs, privateEnv, io, dependencies);
|
|
325
|
+
const terminalStatus = !result.error && result.status === 0 ? "sent"
|
|
326
|
+
: definitiveProviderRejection(result) ? "failed" : null;
|
|
327
|
+
if (terminalStatus) {
|
|
328
|
+
store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
329
|
+
state.document_comment_replies ??= {};
|
|
330
|
+
state.document_comment_replies[input.messageId] = {
|
|
331
|
+
digest,
|
|
332
|
+
status: terminalStatus,
|
|
333
|
+
updated_at: new Date().toISOString(),
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return emitNativeResult(result, io);
|
|
338
|
+
}
|
|
339
|
+
function definitiveProviderRejection(result) {
|
|
340
|
+
if (result.error || result.signal || result.status === null || result.status === 0)
|
|
341
|
+
return false;
|
|
342
|
+
for (const text of [result.stdout, result.stderr]) {
|
|
343
|
+
let value;
|
|
344
|
+
try {
|
|
345
|
+
value = JSON.parse(text || "");
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
351
|
+
continue;
|
|
352
|
+
const response = value;
|
|
353
|
+
const error = response.error && typeof response.error === "object" && !Array.isArray(response.error)
|
|
354
|
+
? response.error : null;
|
|
355
|
+
if ((response.ok === false || (typeof response.code === "number" && response.code !== 0))
|
|
356
|
+
&& ((typeof response.code === "number" && response.code !== 0)
|
|
357
|
+
|| (typeof error?.code === "number" && error.code !== 0)))
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
function callNative(argv, env, io, dependencies) {
|
|
363
|
+
const native = dependencies.nativeCommand;
|
|
364
|
+
const result = (dependencies.spawn ?? spawnSync)(native?.command ?? resolveOfficialLarkCli({ spawn: dependencies.spawn, env }).command, [...(native?.argsPrefix ?? []), ...argv], { encoding: "utf8", env: { ...process.env, ...env } });
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
function spawnNative(argv, env, io, dependencies) {
|
|
368
|
+
const result = callNative(argv, env, io, dependencies);
|
|
369
|
+
if (result.stdout)
|
|
370
|
+
io.stdout(result.stdout);
|
|
371
|
+
if (result.stderr)
|
|
372
|
+
io.stderr(result.stderr);
|
|
373
|
+
if (result.error) {
|
|
374
|
+
io.stderr(`lark-cli: official launcher failed: ${result.error.message}\n`);
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
return result.status ?? (result.signal ? portableSignalCode(result.signal) : 1);
|
|
378
|
+
}
|
|
379
|
+
async function spawnNativeTransparent(argv, env, native) {
|
|
380
|
+
return await new Promise((resolve) => {
|
|
381
|
+
const child = spawn(native.command, [...native.argsPrefix, ...argv], {
|
|
382
|
+
cwd: process.cwd(), env: { ...process.env, ...env }, stdio: "inherit",
|
|
383
|
+
});
|
|
384
|
+
const forward = (signal) => {
|
|
385
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
386
|
+
child.kill(signal);
|
|
387
|
+
};
|
|
388
|
+
const onSigint = () => forward("SIGINT");
|
|
389
|
+
const onSigterm = () => forward("SIGTERM");
|
|
390
|
+
process.on("SIGINT", onSigint);
|
|
391
|
+
process.on("SIGTERM", onSigterm);
|
|
392
|
+
const cleanup = () => {
|
|
393
|
+
process.off("SIGINT", onSigint);
|
|
394
|
+
process.off("SIGTERM", onSigterm);
|
|
395
|
+
};
|
|
396
|
+
child.once("error", (error) => {
|
|
397
|
+
cleanup();
|
|
398
|
+
process.stderr.write(`lark-cli: official launcher failed: ${error.message}\n`);
|
|
399
|
+
resolve(1);
|
|
400
|
+
});
|
|
401
|
+
child.once("exit", (code, signal) => {
|
|
402
|
+
cleanup();
|
|
403
|
+
if (!signal)
|
|
404
|
+
return resolve(code ?? 1);
|
|
405
|
+
const fallback = portableSignalCode(signal);
|
|
406
|
+
try {
|
|
407
|
+
process.kill(process.pid, signal);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
resolve(fallback);
|
|
411
|
+
}
|
|
412
|
+
// A registered/unsupported platform signal may not terminate the process.
|
|
413
|
+
setImmediate(() => resolve(fallback));
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function guardedTarget(decision, argv, store) {
|
|
418
|
+
if (decision.operation === "send") {
|
|
419
|
+
const chatId = policyFlagValue(argv, "--chat-id");
|
|
420
|
+
const userId = policyFlagValue(argv, "--user-id");
|
|
421
|
+
if (!chatId || userId)
|
|
422
|
+
throw new Error("Runtime +messages-send 必须只使用 Inbox 已确认的 --chat-id;--user-id 无法建立 freshness target");
|
|
423
|
+
return feishuImTarget(`chat:${chatId}`);
|
|
424
|
+
}
|
|
425
|
+
const messageId = policyFlagValue(argv, "--message-id");
|
|
426
|
+
if (!messageId)
|
|
427
|
+
throw new Error(`${decision.operation} 写入缺少 --message-id`);
|
|
428
|
+
const target = store.resolveInboxMessageTarget(messageId);
|
|
429
|
+
if (!target)
|
|
430
|
+
throw new Error(`无法从 Inbox 状态确定 ${messageId} 的 target;先 poll 对应消息,禁止旁路 freshness`);
|
|
431
|
+
return feishuImTarget(target);
|
|
432
|
+
}
|
|
433
|
+
function botArgv(argv, intentId) {
|
|
434
|
+
const next = [...argv];
|
|
435
|
+
const parsed = parsePolicyArgv(next);
|
|
436
|
+
const boundary = next.indexOf("--");
|
|
437
|
+
const insertion = boundary < 0 ? next.length : boundary;
|
|
438
|
+
const injected = [];
|
|
439
|
+
if (!parsed.flags.has("--as"))
|
|
440
|
+
injected.push("--as", "bot");
|
|
441
|
+
if (!parsed.flags.has("--idempotency-key"))
|
|
442
|
+
injected.push("--idempotency-key", intentId);
|
|
443
|
+
next.splice(insertion, 0, ...injected);
|
|
444
|
+
return next;
|
|
445
|
+
}
|
|
446
|
+
function probeArgv(target) {
|
|
447
|
+
if (target.provider !== "feishu.im")
|
|
448
|
+
throw new Error(`unsupported freshness provider: ${target.provider}`);
|
|
449
|
+
if (target.resourceKind === "chat") {
|
|
450
|
+
return ["api", "GET", "/open-apis/im/v1/messages", "--params", JSON.stringify({
|
|
451
|
+
container_id_type: "chat", container_id: target.resourceId, sort_type: "ByCreateTimeDesc", page_size: 20,
|
|
452
|
+
}), "--as", "bot"];
|
|
453
|
+
}
|
|
454
|
+
if (target.resourceKind === "thread") {
|
|
455
|
+
const slash = target.resourceId.indexOf("/");
|
|
456
|
+
const threadId = slash >= 0 ? target.resourceId.slice(slash + 1) : "";
|
|
457
|
+
if (!threadId)
|
|
458
|
+
throw new Error("thread freshness target is malformed");
|
|
459
|
+
return ["api", "GET", "/open-apis/im/v1/messages", "--params", JSON.stringify({
|
|
460
|
+
container_id_type: "thread", container_id: threadId, sort_type: "ByCreateTimeDesc", page_size: 20,
|
|
461
|
+
}), "--as", "bot"];
|
|
462
|
+
}
|
|
463
|
+
throw new Error(`unsupported freshness resource: ${target.resourceKind}`);
|
|
464
|
+
}
|
|
465
|
+
function parseHistory(result, target, requireBotIdentity = false) {
|
|
466
|
+
if (result.error)
|
|
467
|
+
throw new Error(`authoritative history probe failed: ${result.error.message}`);
|
|
468
|
+
if (result.status !== 0)
|
|
469
|
+
throw new Error(`authoritative history probe exited ${result.status ?? "without status"}: ${result.stderr || "no details"}`);
|
|
470
|
+
let value;
|
|
471
|
+
try {
|
|
472
|
+
value = JSON.parse(result.stdout || "");
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
throw new Error("authoritative history probe returned non-JSON output");
|
|
476
|
+
}
|
|
477
|
+
const root = value;
|
|
478
|
+
if (!root || root.ok !== true || !root.data)
|
|
479
|
+
throw new Error("authoritative history probe returned an unsuccessful payload");
|
|
480
|
+
if (requireBotIdentity && root.identity !== "bot")
|
|
481
|
+
throw new Error("authoritative history probe did not confirm Bot identity");
|
|
482
|
+
const rows = root.data.messages ?? root.data.items;
|
|
483
|
+
if (!Array.isArray(rows))
|
|
484
|
+
throw new Error("authoritative history probe omitted messages");
|
|
485
|
+
const messages = rows;
|
|
486
|
+
for (const message of messages) {
|
|
487
|
+
if (!message || typeof message !== "object" || typeof message.message_id !== "string" || !message.message_id) {
|
|
488
|
+
throw new Error("authoritative history payload contained a message without message_id");
|
|
489
|
+
}
|
|
490
|
+
if (target.resourceKind === "chat" && message.chat_id !== target.resourceId) {
|
|
491
|
+
throw new Error("authoritative history payload crossed chat target boundary");
|
|
492
|
+
}
|
|
493
|
+
if (target.resourceKind === "thread") {
|
|
494
|
+
const [chatId, threadId] = target.resourceId.split("/", 2);
|
|
495
|
+
if (message.chat_id !== chatId || message.thread_id !== threadId) {
|
|
496
|
+
throw new Error("authoritative history payload crossed thread target boundary");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return { messages };
|
|
501
|
+
}
|
|
502
|
+
function intentId(target, cursor, argv) {
|
|
503
|
+
const fingerprint = createHash("sha256")
|
|
504
|
+
.update(JSON.stringify([target, cursor, argv])).digest("hex");
|
|
505
|
+
return `larkin-${fingerprint.slice(0, 32)}`;
|
|
506
|
+
}
|
|
507
|
+
function emitFreshnessError(io, input) {
|
|
508
|
+
io.stderr(`${JSON.stringify({
|
|
509
|
+
ok: false,
|
|
510
|
+
identity: "bot",
|
|
511
|
+
error: { type: input.subtype === "freshness_conflict" ? "conflict" : "unavailable", subtype: input.subtype,
|
|
512
|
+
...(input.reason ? { message: input.reason } : {}) },
|
|
513
|
+
target: input.target,
|
|
514
|
+
...(input.current ? { current_cursor: input.current } : {}),
|
|
515
|
+
...(input.messages ? { unseen_messages: input.messages } : {}),
|
|
516
|
+
next: "Reconsider the returned context, then retry the ordinary send/reply/card command; history is probed again before every write.",
|
|
517
|
+
})}\n`);
|
|
518
|
+
}
|
|
519
|
+
function freshnessGeneration(env) {
|
|
520
|
+
return typeof env.LARKIN_RUNTIME_OBSERVATION_GENERATION === "string" && env.LARKIN_RUNTIME_OBSERVATION_GENERATION
|
|
521
|
+
? env.LARKIN_RUNTIME_OBSERVATION_GENERATION : "external";
|
|
522
|
+
}
|
|
523
|
+
function rawFlagValue(argv, flag) {
|
|
524
|
+
const nativeArgv = nativeArgvBeforeBoundary(argv);
|
|
525
|
+
const direct = nativeArgv.indexOf(flag);
|
|
526
|
+
if (direct >= 0)
|
|
527
|
+
return nativeArgv[direct + 1] ?? null;
|
|
528
|
+
const inline = nativeArgv.find((argument) => argument.startsWith(`${flag}=`));
|
|
529
|
+
return inline ? inline.slice(flag.length + 1) : null;
|
|
530
|
+
}
|
|
531
|
+
function historyShortcut(argv) {
|
|
532
|
+
const positional = [];
|
|
533
|
+
const nativeArgv = nativeArgvBeforeBoundary(argv);
|
|
534
|
+
for (let index = 0; index < nativeArgv.length; index += 1) {
|
|
535
|
+
const argument = nativeArgv[index];
|
|
536
|
+
const inlineFlag = [...HISTORY_VALUE_FLAGS].find((flag) => argument.startsWith(`${flag}=`));
|
|
537
|
+
if (inlineFlag)
|
|
538
|
+
continue;
|
|
539
|
+
if (HISTORY_VALUE_FLAGS.has(argument)) {
|
|
540
|
+
index += 1;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (argument.startsWith("-"))
|
|
544
|
+
continue;
|
|
545
|
+
positional.push(argument);
|
|
546
|
+
}
|
|
547
|
+
if (positional.length !== 2 || positional[0] !== "im")
|
|
548
|
+
return null;
|
|
549
|
+
return positional[1] === "+chat-messages-list" || positional[1] === "+threads-messages-list"
|
|
550
|
+
? positional[1] : null;
|
|
551
|
+
}
|
|
552
|
+
function boundedHistoryArgv(argv) {
|
|
553
|
+
if (!historyShortcut(argv))
|
|
554
|
+
return [...argv];
|
|
555
|
+
const boundary = argv.indexOf("--");
|
|
556
|
+
const commandArgv = boundary < 0 ? argv : argv.slice(0, boundary);
|
|
557
|
+
if (commandArgv.includes("--page-size")
|
|
558
|
+
|| commandArgv.some((argument) => argument.startsWith("--page-size=")))
|
|
559
|
+
return [...argv];
|
|
560
|
+
const next = [...argv];
|
|
561
|
+
next.splice(boundary < 0 ? next.length : boundary, 0, "--page-size", "20");
|
|
562
|
+
return next;
|
|
563
|
+
}
|
|
564
|
+
function conditionalHeadReadTarget(argv) {
|
|
565
|
+
if (["--page-token", "--start", "--end", "--jq", "-q"].some((flag) => argv.includes(flag)
|
|
566
|
+
|| argv.some((argument) => argument.startsWith(`${flag}=`))))
|
|
567
|
+
return null;
|
|
568
|
+
const format = rawFlagValue(argv, "--format");
|
|
569
|
+
if (format && format !== "json")
|
|
570
|
+
return null;
|
|
571
|
+
if (historyShortcut(argv) === "+chat-messages-list") {
|
|
572
|
+
const chatId = rawFlagValue(argv, "--chat-id");
|
|
573
|
+
const order = rawFlagValue(argv, "--order") ?? "desc";
|
|
574
|
+
return chatId && order === "desc" ? feishuImTarget(`chat:${chatId}`) : null;
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
function eligibleThreadHeadRead(argv) {
|
|
579
|
+
if (historyShortcut(argv) !== "+threads-messages-list")
|
|
580
|
+
return null;
|
|
581
|
+
if (["--page-token", "--jq", "-q"].some((flag) => argv.includes(flag)
|
|
582
|
+
|| argv.some((argument) => argument.startsWith(`${flag}=`))))
|
|
583
|
+
return null;
|
|
584
|
+
const format = rawFlagValue(argv, "--format");
|
|
585
|
+
const order = rawFlagValue(argv, "--order") ?? "asc";
|
|
586
|
+
return (!format || format === "json") && order === "desc" ? rawFlagValue(argv, "--thread") : null;
|
|
587
|
+
}
|
|
588
|
+
function displayedHeadIds(result, target) {
|
|
589
|
+
if (result.error || result.status !== 0)
|
|
590
|
+
throw new Error("displayed history read failed");
|
|
591
|
+
const value = JSON.parse(result.stdout || "");
|
|
592
|
+
const rows = value?.ok === true && value.data ? (value.data.messages ?? value.data.items) : null;
|
|
593
|
+
if (!Array.isArray(rows))
|
|
594
|
+
throw new Error("displayed history omitted messages");
|
|
595
|
+
const ids = [];
|
|
596
|
+
for (const row of rows) {
|
|
597
|
+
if (!row || typeof row !== "object")
|
|
598
|
+
throw new Error("displayed history contained a malformed message");
|
|
599
|
+
const message = row;
|
|
600
|
+
if (typeof message.message_id !== "string" || !message.message_id)
|
|
601
|
+
throw new Error("displayed history omitted message_id");
|
|
602
|
+
if (target.resourceKind === "chat" && message.chat_id !== target.resourceId)
|
|
603
|
+
throw new Error("displayed history crossed chat target boundary");
|
|
604
|
+
if (target.resourceKind === "thread") {
|
|
605
|
+
const [chatId, threadId] = target.resourceId.split("/", 2);
|
|
606
|
+
if (message.chat_id !== chatId || message.thread_id !== threadId)
|
|
607
|
+
throw new Error("displayed history crossed thread target boundary");
|
|
608
|
+
}
|
|
609
|
+
ids.push(message.message_id);
|
|
610
|
+
}
|
|
611
|
+
return ids;
|
|
612
|
+
}
|
|
613
|
+
function passthroughWithObservation(argv, env, io, dependencies, store) {
|
|
614
|
+
const effectiveArgv = boundedHistoryArgv(argv);
|
|
615
|
+
const result = callNative(effectiveArgv, env, io, dependencies);
|
|
616
|
+
let target = conditionalHeadReadTarget(effectiveArgv);
|
|
617
|
+
const threadLocator = eligibleThreadHeadRead(effectiveArgv);
|
|
618
|
+
if (!target && threadLocator && !result.error && result.status === 0) {
|
|
619
|
+
try {
|
|
620
|
+
const value = JSON.parse(result.stdout || "");
|
|
621
|
+
const rows = value?.ok === true && value.data ? (value.data.messages ?? value.data.items) : null;
|
|
622
|
+
if (Array.isArray(rows) && rows.length > 0) {
|
|
623
|
+
const locators = new Set(rows.map((row) => {
|
|
624
|
+
if (!row || typeof row !== "object")
|
|
625
|
+
return "";
|
|
626
|
+
const message = row;
|
|
627
|
+
return typeof message.chat_id === "string" && typeof message.thread_id === "string"
|
|
628
|
+
? `${message.chat_id}:${message.thread_id}` : "";
|
|
629
|
+
}));
|
|
630
|
+
if (locators.size === 1 && !locators.has("")) {
|
|
631
|
+
const [chatId, threadId] = [...locators][0].split(":", 2);
|
|
632
|
+
if (threadId === threadLocator || threadLocator.startsWith("om_"))
|
|
633
|
+
target = feishuImTarget(`thread:${chatId}:${threadId}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch { /* non-JSON successful output cannot establish a head cursor */ }
|
|
638
|
+
}
|
|
639
|
+
if (target && !result.error && result.status === 0) {
|
|
640
|
+
try {
|
|
641
|
+
const displayedIds = displayedHeadIds(result, target);
|
|
642
|
+
const rawSnapshot = parseHistory(callNative(probeArgv(target), env, io, dependencies), target, true);
|
|
643
|
+
const rawIds = rawSnapshot.messages.map((message) => message.message_id);
|
|
644
|
+
if (displayedIds.length !== rawIds.length
|
|
645
|
+
|| new Set(displayedIds).size !== displayedIds.length
|
|
646
|
+
|| new Set(rawIds).size !== rawIds.length
|
|
647
|
+
|| displayedIds.some((id) => !rawIds.includes(id))) {
|
|
648
|
+
throw new Error("displayed and raw history heads did not reconcile");
|
|
649
|
+
}
|
|
650
|
+
const cursor = feishuImFreshnessAdapter.cursor(rawSnapshot);
|
|
651
|
+
if (cursor)
|
|
652
|
+
store.mergeFreshnessCursor(serializeFeishuImTarget(target), cursor, mergeFeishuImCursor, freshnessGeneration(env));
|
|
653
|
+
}
|
|
654
|
+
catch { /* successful displayed bytes remain authoritative; failed raw reconciliation never advances seen */ }
|
|
655
|
+
}
|
|
656
|
+
if (result.stdout)
|
|
657
|
+
io.stdout(result.stdout);
|
|
658
|
+
if (result.stderr)
|
|
659
|
+
io.stderr(result.stderr);
|
|
660
|
+
if (result.error) {
|
|
661
|
+
io.stderr(`lark-cli: official launcher failed: ${result.error.message}\n`);
|
|
662
|
+
return 1;
|
|
663
|
+
}
|
|
664
|
+
return result.status ?? (result.signal ? portableSignalCode(result.signal) : 1);
|
|
665
|
+
}
|
|
666
|
+
function requiresCapturedPassthrough(argv) {
|
|
667
|
+
const effectiveArgv = boundedHistoryArgv(argv);
|
|
668
|
+
return conditionalHeadReadTarget(effectiveArgv) !== null || eligibleThreadHeadRead(effectiveArgv) !== null;
|
|
669
|
+
}
|
|
670
|
+
function writeResponseMessage(result) {
|
|
671
|
+
try {
|
|
672
|
+
const value = JSON.parse(result.stdout || "");
|
|
673
|
+
if (value?.ok !== true || !value.data || typeof value.data !== "object" || Array.isArray(value.data))
|
|
674
|
+
return null;
|
|
675
|
+
const data = value.data;
|
|
676
|
+
const candidate = data.message && typeof data.message === "object" && !Array.isArray(data.message)
|
|
677
|
+
? data.message : data;
|
|
678
|
+
return typeof candidate.message_id === "string" && candidate.message_id ? candidate : null;
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function observeSuccessfulWrite(result, target, targetKey, store, generation) {
|
|
685
|
+
if (result.error || result.status !== 0)
|
|
686
|
+
return false;
|
|
687
|
+
try {
|
|
688
|
+
const candidate = writeResponseMessage(result);
|
|
689
|
+
if (!candidate)
|
|
690
|
+
return false;
|
|
691
|
+
const snapshot = parseHistory({ ...result, stdout: JSON.stringify({ ok: true, data: { messages: [candidate] } }) }, target);
|
|
692
|
+
const cursor = feishuImFreshnessAdapter.cursor(snapshot);
|
|
693
|
+
if (cursor)
|
|
694
|
+
store.mergeFreshnessCursor(targetKey, cursor, mergeFeishuImCursor, generation);
|
|
695
|
+
return cursor !== null;
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function emitNativeResult(result, io) {
|
|
702
|
+
if (result.stdout)
|
|
703
|
+
io.stdout(result.stdout);
|
|
704
|
+
if (result.stderr)
|
|
705
|
+
io.stderr(result.stderr);
|
|
706
|
+
if (result.error) {
|
|
707
|
+
io.stderr(`lark-cli: official launcher failed: ${result.error.message}\n`);
|
|
708
|
+
return 1;
|
|
709
|
+
}
|
|
710
|
+
return result.status ?? (result.signal ? portableSignalCode(result.signal) : 1);
|
|
711
|
+
}
|
|
712
|
+
function emitCommittedUnverified(result, io, input) {
|
|
713
|
+
let providerResponse;
|
|
714
|
+
try {
|
|
715
|
+
providerResponse = JSON.parse(result.stdout || "");
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
providerResponse = { raw_stdout: result.stdout || "" };
|
|
719
|
+
}
|
|
720
|
+
const providerDocument = providerResponse && typeof providerResponse === "object" && !Array.isArray(providerResponse)
|
|
721
|
+
? providerResponse
|
|
722
|
+
: { provider_response: providerResponse };
|
|
723
|
+
io.stdout(`${JSON.stringify({
|
|
724
|
+
...providerDocument,
|
|
725
|
+
ok: true,
|
|
726
|
+
committed: true,
|
|
727
|
+
verified: false,
|
|
728
|
+
cursor_advanced: false,
|
|
729
|
+
target: input.target,
|
|
730
|
+
verification: {
|
|
731
|
+
status: "unverified",
|
|
732
|
+
subtype: "post_write_unverified",
|
|
733
|
+
message: input.reason,
|
|
734
|
+
...(input.current ? { current_cursor: input.current } : {}),
|
|
735
|
+
...(input.messages ? { unseen_messages: input.messages } : {}),
|
|
736
|
+
},
|
|
737
|
+
...(result.stderr ? { provider_stderr_present: true } : {}),
|
|
738
|
+
})}\n`);
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
742
|
+
const io = dependencies.io ?? defaultIo();
|
|
743
|
+
const runtimeAgentId = larkinConfig.resolveRuntimeAuthority(env);
|
|
744
|
+
if (!runtimeAgentId) {
|
|
745
|
+
try {
|
|
746
|
+
const nativeDependencies = dependencies.nativeCommand ? dependencies
|
|
747
|
+
: { ...dependencies, nativeCommand: resolveOfficialLarkCli({ spawn: dependencies.spawn, env }) };
|
|
748
|
+
return spawnNative(argv, env, io, nativeDependencies);
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
io.stderr(`lark-cli: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
752
|
+
return 1;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
let config;
|
|
756
|
+
let agent;
|
|
757
|
+
try {
|
|
758
|
+
({ config } = larkinConfig.loadConfig(env));
|
|
759
|
+
agent = larkinConfig.selectAgent(config, { ...env, LARKIN_AGENT_ID: runtimeAgentId });
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
io.stderr(`lark-cli: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
763
|
+
return 2;
|
|
764
|
+
}
|
|
765
|
+
const decision = classifyLarkCliCommand(argv);
|
|
766
|
+
if (decision.kind === "denied") {
|
|
767
|
+
io.stderr(`lark-cli: ${decision.reason}\n`);
|
|
768
|
+
return 2;
|
|
769
|
+
}
|
|
770
|
+
const privateEnv = managedLarkCliEnv(agent, { ...env, LARKIN_AGENT_ID: agent.agentId });
|
|
771
|
+
let nativeDependencies;
|
|
772
|
+
try {
|
|
773
|
+
nativeDependencies = dependencies.nativeCommand ? dependencies
|
|
774
|
+
: { ...dependencies, nativeCommand: resolveOfficialLarkCli({ spawn: dependencies.spawn, env: privateEnv }) };
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
io.stderr(`lark-cli: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
778
|
+
return 2;
|
|
779
|
+
}
|
|
780
|
+
const store = dependencies.stateStore ?? createAgentStateStore(config.larkinHome, agent.agentId);
|
|
781
|
+
if (decision.kind === "comment-reply") {
|
|
782
|
+
try {
|
|
783
|
+
let telemetry = dependencies.telemetry ?? telemetrySingleton();
|
|
784
|
+
if (!dependencies.telemetry) {
|
|
785
|
+
try {
|
|
786
|
+
const sourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
787
|
+
telemetry = telemetrySingleton(loadTelemetryConfig(env), { serviceVersion: packageVersion(sourceRoot) });
|
|
788
|
+
}
|
|
789
|
+
catch { /* telemetry is failure-isolated */ }
|
|
790
|
+
}
|
|
791
|
+
return telemetry.externalPhase(agent.agentId, store.paths.root, "document.comment.reply", SpanKind.CLIENT, () => runCommentReply(argv, privateEnv, io, nativeDependencies, store), "comment_cli");
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
io.stderr(`lark-cli: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
795
|
+
return 2;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (decision.kind === "passthrough")
|
|
799
|
+
return passthroughWithObservation(argv, privateEnv, io, nativeDependencies, store);
|
|
800
|
+
try {
|
|
801
|
+
const target = guardedTarget(decision, argv, store);
|
|
802
|
+
const targetKey = serializeFeishuImTarget(target);
|
|
803
|
+
const generation = freshnessGeneration(privateEnv);
|
|
804
|
+
const seen = store.readFreshnessCursor(targetKey, generation);
|
|
805
|
+
const gated = evaluateFreshness({
|
|
806
|
+
seen,
|
|
807
|
+
adapter: feishuImFreshnessAdapter,
|
|
808
|
+
probe: () => parseHistory(callNative(probeArgv(target), privateEnv, io, nativeDependencies), target, true),
|
|
809
|
+
});
|
|
810
|
+
if (gated.status === "unavailable") {
|
|
811
|
+
emitFreshnessError(io, { subtype: "freshness_unavailable", target: targetKey, reason: gated.reason });
|
|
812
|
+
return 3;
|
|
813
|
+
}
|
|
814
|
+
if (gated.status === "conflict") {
|
|
815
|
+
emitFreshnessError(io, { subtype: "freshness_conflict", target: targetKey, current: gated.current, messages: gated.context });
|
|
816
|
+
store.mergeFreshnessCursor(targetKey, gated.current, mergeFeishuImCursor, generation);
|
|
817
|
+
return 3;
|
|
818
|
+
}
|
|
819
|
+
const write = callNative(botArgv(argv, intentId(targetKey, gated.current, argv)), privateEnv, io, nativeDependencies);
|
|
820
|
+
if (!write.error && write.status === 0 && !observeSuccessfulWrite(write, target, targetKey, store, generation)) {
|
|
821
|
+
const responseMessage = writeResponseMessage(write);
|
|
822
|
+
try {
|
|
823
|
+
const postSnapshot = parseHistory(callNative(probeArgv(target), privateEnv, io, nativeDependencies), target, true);
|
|
824
|
+
const postCursor = feishuImFreshnessAdapter.cursor(postSnapshot);
|
|
825
|
+
const unseenAfterWrite = feishuImFreshnessAdapter.unseen(gated.current, postSnapshot);
|
|
826
|
+
const confirmedOwnWrite = responseMessage && postCursor
|
|
827
|
+
&& unseenAfterWrite.some((message) => message.message_id === responseMessage.message_id)
|
|
828
|
+
&& unseenAfterWrite.every((message) => message.message_id === responseMessage.message_id);
|
|
829
|
+
if (!confirmedOwnWrite) {
|
|
830
|
+
return emitCommittedUnverified(write, io, {
|
|
831
|
+
target: targetKey,
|
|
832
|
+
...(postCursor ? { current: postCursor } : {}),
|
|
833
|
+
messages: unseenAfterWrite,
|
|
834
|
+
reason: responseMessage
|
|
835
|
+
? "provider write succeeded but bounded post-write probe found an additional concurrent update; cursor was not advanced"
|
|
836
|
+
: "provider write succeeded without a message id/revision and bounded post-write probe could not identify the write; cursor was not advanced",
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
store.mergeFreshnessCursor(targetKey, postCursor, mergeFeishuImCursor, generation);
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
return emitCommittedUnverified(write, io, {
|
|
843
|
+
target: targetKey,
|
|
844
|
+
reason: "provider write succeeded but bounded post-write confirmation was unavailable; cursor was not advanced",
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return emitNativeResult(write, io);
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
io.stderr(`lark-cli: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
852
|
+
return 2;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
export async function runLarkCliProcess(argv, env = process.env) {
|
|
856
|
+
const runtimeAgentId = larkinConfig.resolveRuntimeAuthority(env);
|
|
857
|
+
if (!runtimeAgentId) {
|
|
858
|
+
try {
|
|
859
|
+
return await spawnNativeTransparent(argv, env, resolveOfficialLarkCli({ env }));
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
process.stderr.write(`lark-cli: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
863
|
+
return 1;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
const { config } = larkinConfig.loadConfig(env);
|
|
868
|
+
const agent = larkinConfig.selectAgent(config, { ...env, LARKIN_AGENT_ID: runtimeAgentId });
|
|
869
|
+
assertAgentWorkspaceBound(agent);
|
|
870
|
+
const decision = classifyLarkCliCommand(argv);
|
|
871
|
+
if (decision.kind === "passthrough" && !requiresCapturedPassthrough(argv)) {
|
|
872
|
+
const privateEnv = managedLarkCliEnv(agent, { ...env, LARKIN_AGENT_ID: agent.agentId });
|
|
873
|
+
return await spawnNativeTransparent(argv, privateEnv, resolveOfficialLarkCli({ env: privateEnv }));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
process.stderr.write(`lark-cli: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
878
|
+
return 2;
|
|
879
|
+
}
|
|
880
|
+
return runLarkCli(argv, env);
|
|
881
|
+
}
|
|
882
|
+
export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
883
|
+
process.exit(await runLarkCliProcess(argv, env));
|
|
884
|
+
}
|
|
885
|
+
const entry = process.argv[1];
|
|
886
|
+
if (entry && path.resolve(entry) === path.resolve(fileURLToPath(import.meta.url)))
|
|
887
|
+
void main();
|