larkin 0.2.75 → 0.2.77
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/README.md +3 -3
- package/dist/agent/context-prompt.cjs +2 -2
- package/dist/agent/context-prompt.mjs +2 -2
- package/dist/app/agent-cli.mjs +3 -3
- package/dist/app/agent-lark-cli-workspace.cjs +3 -2
- package/dist/app/agent-lark-cli-workspace.mjs +3 -2
- package/dist/app/cli.mjs +1 -1
- package/dist/app/lark-cli.mjs +53 -3
- package/dist/app/local-control.mjs +11 -6
- package/dist/app/official-lark-cli.cjs +66 -7
- package/dist/app/official-lark-cli.mjs +66 -7
- package/dist/app/runtime-agent-config.mjs +3 -2
- package/dist/app/setup.mjs +4 -4
- package/dist/platform/callback-capability.cjs +2 -1
- package/dist/platform/callback-capability.mjs +2 -1
- package/dist/platform/config.cjs +3 -8
- package/dist/platform/config.mjs +3 -8
- package/dist/platform/release-artifacts.mjs +10 -3
- package/dist/platform/secure-metadata.cjs +48 -0
- package/dist/platform/secure-metadata.mjs +38 -0
- package/dist/platform/telemetry-spool.cjs +3 -14
- package/dist/platform/telemetry-spool.mjs +3 -14
- package/dist/platform/telemetry-tracing.cjs +2 -7
- package/dist/platform/telemetry-tracing.mjs +2 -7
- package/dist/setup/bot-register.mjs +5 -5
- package/dist/setup/grant-scopes.mjs +3 -3
- package/dist/setup/setup-bind.mjs +3 -3
- package/package.json +5 -3
- package/scripts/npm/install-binary.mjs +125 -0
- package/scripts/npm/larkin-bin-shim.mjs +48 -0
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Larkin
|
|
2
2
|
|
|
3
|
-
Larkin connects Codex, Claude Code, and Pi agent runtimes to Feishu. It provides a local runtime host, persistent sessions, reminders, interactive messages, and a local dashboard.
|
|
3
|
+
Larkin connects Codex, Claude Code, and Pi agent runtimes to Feishu (Lark). It provides a local runtime host, persistent sessions, reminders, interactive messages, and a local dashboard.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
|
-
- A supported macOS or
|
|
7
|
+
- A supported macOS, Linux, or Windows (x64) system
|
|
8
8
|
- `lark-cli`
|
|
9
9
|
- At least one supported agent runtime and its authentication
|
|
10
10
|
- Bun 1.3.14 when running the npm package or building from source (standalone binaries bundle their own runtime)
|
|
@@ -22,7 +22,7 @@ npm install -g larkin
|
|
|
22
22
|
larkin --version
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Standalone binaries for macOS and
|
|
25
|
+
Standalone binaries for macOS, Linux, and Windows (x64) are attached to every [GitHub Release](https://github.com/eddiearc/larkin/releases) for environments without Bun or npm. Windows binaries are cross-compiled; their runtime behavior has not been end-to-end verified on a Windows host.
|
|
26
26
|
|
|
27
27
|
## Usage
|
|
28
28
|
|
|
@@ -16,7 +16,7 @@ const agent_cli_capabilities_js_1 = require("./agent-cli-capabilities.cjs");
|
|
|
16
16
|
* 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
|
|
17
17
|
* 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
|
|
18
18
|
*/
|
|
19
|
-
exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-
|
|
19
|
+
exports.LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v19";
|
|
20
20
|
/**
|
|
21
21
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
22
22
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -154,7 +154,7 @@ class ContextPromptBuilder {
|
|
|
154
154
|
"For the known canonical Inbox poll, scoped chat/thread history, and exact send/reply recipes specified here, execute them directly; you must not read or re-read a skill file or reference merely to rediscover or confirm their command syntax.",
|
|
155
155
|
`For a task that starts from an exact group name and asks for user and bot counts, after the required Inbox poll use this known canonical two-read recipe directly: first run \`${executable} im +chat-search --query '<exact_group_name>' --json\`, require one result with an exact name match and confirm its \`oc_\` chat id, then run \`${executable} im chats get --chat-id <confirmed_oc_chat_id> --json\` and answer from its \`user_count\` and \`bot_count\`. This group user/bot counts recipe uses exactly two post-poll business read calls; you must not read or re-read a skill or reference, open help or schema, invoke bare \`lark-cli\`, call \`chat.members\` or \`+chat-members-list\`, fall back to \`+chat-list\`, or add shell filters or output truncation. If the exact name is not a unique visible match, the chat id is not confirmed, or either count is missing, fail visibly without another discovery path or a guess.`,
|
|
156
156
|
"This narrow direct-recipe rule does not waive required skill safety gates, authorization, identity, freshness, or other safety checks. Unknown commands or high-risk operations still require the applicable skill/reference guidance or help.",
|
|
157
|
-
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe.",
|
|
157
|
+
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe. A successful write result carrying `duplicate: true` means the provider returned the earlier delivery for the same derived key: the message was already delivered and no new message was created. Do not resend the same command after a `duplicate: true` result; treat the message as delivered. To deliberately send identical content again as a fresh intent (e.g., a new reminder), pass an explicit new `--idempotency-key`, which the wrapper respects instead of deriving its own.",
|
|
158
158
|
"Both `freshness_unavailable` and `freshness_conflict` are pre-commit, provider-not-reached results. If a retry is warranted for an unchanged exact operation, rerun the identical safe ordinary command: `--text` for a direct literal or the same deterministic `--content` dataflow for a tool source. The wrapper reuses its derived key. If the target or body changes after reconsideration, run the revised ordinary command and let the wrapper derive a new key. A result with `committed=true` must not be repeated; ambiguous termination follows the existing wrapper same-key recovery contract.",
|
|
159
159
|
"For multiline `--markdown` or `--text` content in zsh or bash, prefer shell ANSI-C quoting so the command passes one argument with real newline characters: `--markdown $'First line\\nSecond line'` or `--text $'First line\\nSecond line'`. Putting `\"First line\\nSecond line\"` in ordinary double quotes is wrong: ordinary quotes do not decode `\\n`, so lark-cli and Feishu receive a backslash followed by the letter `n`. A literal multiline argument containing real newline characters is also valid. If ANSI-C-quoted content contains an apostrophe, use a safe shell single-quote splice or the literal-newline form; never use `eval`, `echo`, a temporary file, or unsafe variable interpolation to construct the body.",
|
|
160
160
|
...FEISHU_IM_COMMAND_GROUPS.map(([label, suffixes]) => `- ${label}: ${suffixes.map((suffix) => `\`${executable} ${suffix}\``).join(", ")}.`),
|
|
@@ -13,7 +13,7 @@ import { agentCliPromptCapabilities } from "./agent-cli-capabilities.mjs";
|
|
|
13
13
|
* 5. 版本化:每次实质改动 bump LARKIN_STANDING_PROMPT_VERSION,并同步 eval 数据集/graders/断言。
|
|
14
14
|
* 6. 用 eval 验证:行为变化必须配套固定场景 + rubric(evals/*、test/support/*-grader.mjs、live 测试)。
|
|
15
15
|
*/
|
|
16
|
-
export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-
|
|
16
|
+
export const LARKIN_STANDING_PROMPT_VERSION = "larkin-standing-v19";
|
|
17
17
|
/**
|
|
18
18
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
19
19
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -151,7 +151,7 @@ export class ContextPromptBuilder {
|
|
|
151
151
|
"For the known canonical Inbox poll, scoped chat/thread history, and exact send/reply recipes specified here, execute them directly; you must not read or re-read a skill file or reference merely to rediscover or confirm their command syntax.",
|
|
152
152
|
`For a task that starts from an exact group name and asks for user and bot counts, after the required Inbox poll use this known canonical two-read recipe directly: first run \`${executable} im +chat-search --query '<exact_group_name>' --json\`, require one result with an exact name match and confirm its \`oc_\` chat id, then run \`${executable} im chats get --chat-id <confirmed_oc_chat_id> --json\` and answer from its \`user_count\` and \`bot_count\`. This group user/bot counts recipe uses exactly two post-poll business read calls; you must not read or re-read a skill or reference, open help or schema, invoke bare \`lark-cli\`, call \`chat.members\` or \`+chat-members-list\`, fall back to \`+chat-list\`, or add shell filters or output truncation. If the exact name is not a unique visible match, the chat id is not confirmed, or either count is missing, fail visibly without another discovery path or a guess.`,
|
|
153
153
|
"This narrow direct-recipe rule does not waive required skill safety gates, authorization, identity, freshness, or other safety checks. Unknown commands or high-risk operations still require the applicable skill/reference guidance or help.",
|
|
154
|
-
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe.",
|
|
154
|
+
"The Larkin wrapper derives the stable per-intent idempotency key; do not pass `--idempotency-key` in an ordinary send or reply recipe. A successful write result carrying `duplicate: true` means the provider returned the earlier delivery for the same derived key: the message was already delivered and no new message was created. Do not resend the same command after a `duplicate: true` result; treat the message as delivered. To deliberately send identical content again as a fresh intent (e.g., a new reminder), pass an explicit new `--idempotency-key`, which the wrapper respects instead of deriving its own.",
|
|
155
155
|
"Both `freshness_unavailable` and `freshness_conflict` are pre-commit, provider-not-reached results. If a retry is warranted for an unchanged exact operation, rerun the identical safe ordinary command: `--text` for a direct literal or the same deterministic `--content` dataflow for a tool source. The wrapper reuses its derived key. If the target or body changes after reconsideration, run the revised ordinary command and let the wrapper derive a new key. A result with `committed=true` must not be repeated; ambiguous termination follows the existing wrapper same-key recovery contract.",
|
|
156
156
|
"For multiline `--markdown` or `--text` content in zsh or bash, prefer shell ANSI-C quoting so the command passes one argument with real newline characters: `--markdown $'First line\\nSecond line'` or `--text $'First line\\nSecond line'`. Putting `\"First line\\nSecond line\"` in ordinary double quotes is wrong: ordinary quotes do not decode `\\n`, so lark-cli and Feishu receive a backslash followed by the letter `n`. A literal multiline argument containing real newline characters is also valid. If ANSI-C-quoted content contains an apostrophe, use a safe shell single-quote splice or the literal-newline form; never use `eval`, `echo`, a temporary file, or unsafe variable interpolation to construct the body.",
|
|
157
157
|
...FEISHU_IM_COMMAND_GROUPS.map(([label, suffixes]) => `- ${label}: ${suffixes.map((suffix) => `\`${executable} ${suffix}\``).join(", ")}.`),
|
package/dist/app/agent-cli.mjs
CHANGED
|
@@ -105,7 +105,7 @@ function query(requestPath, name) {
|
|
|
105
105
|
}
|
|
106
106
|
function migrationError(group, subcommand) {
|
|
107
107
|
if (group === "message") {
|
|
108
|
-
return "message 已移除:只读查看收件箱用 `larkin inbox check`,领取完整消息用 `larkin inbox poll
|
|
108
|
+
return "message 已移除:只读查看收件箱用 `larkin inbox check`,领取完整消息用 `larkin inbox poll`;飞书(Lark)发送、回复和查询请使用 `larkin im +messages-send`、`+messages-reply`、`+chat-messages-list` 或 `+messages-mget`。";
|
|
109
109
|
}
|
|
110
110
|
if (group === "channel") {
|
|
111
111
|
return "channel 已移除:群聊操作请使用 `larkin im +chat-list`、`+chat-search`、`chats get`、`+chat-create`、`+chat-update` 或 `chat.members get/create/delete`。";
|
|
@@ -114,9 +114,9 @@ function migrationError(group, subcommand) {
|
|
|
114
114
|
return "attachment 已移除:发送附件请使用 `larkin im +messages-send --file/--image/--video/--audio`;下载请使用 `larkin im +messages-resources-download`。";
|
|
115
115
|
}
|
|
116
116
|
if (group === "server")
|
|
117
|
-
return "server
|
|
117
|
+
return "server 已移除;飞书(Lark)群与消息信息请通过 `larkin im ...` 查询。";
|
|
118
118
|
if (group === "im")
|
|
119
|
-
return "请通过 `larkin im ...`
|
|
119
|
+
return "请通过 `larkin im ...` 使用飞书(Lark)命令;Runtime 会自动绑定当前 Bot identity,并在写入前执行 freshness gate。可运行 `larkin im --help` 查看帮助。";
|
|
120
120
|
if (group === "profile" && subcommand !== "show") {
|
|
121
121
|
return "profile 只保留只读的 `larkin profile show`;身份和凭证由 `larkin setup` 管理,不支持 update。";
|
|
122
122
|
}
|
|
@@ -10,6 +10,7 @@ exports.managedOfficialLarkCli = managedOfficialLarkCli;
|
|
|
10
10
|
exports.assertAgentWorkspaceBound = assertAgentWorkspaceBound;
|
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const secure_metadata_js_1 = require("../platform/secure-metadata.cjs");
|
|
13
14
|
const official_lark_cli_js_1 = require("./official-lark-cli.cjs");
|
|
14
15
|
function larkChannelSourceConfigPath(agent) {
|
|
15
16
|
if (!agent.stateDir)
|
|
@@ -43,13 +44,13 @@ function managedOfficialLarkCli(agent, env, resolve = official_lark_cli_js_1.res
|
|
|
43
44
|
}
|
|
44
45
|
function readPrivateJson(file, label) {
|
|
45
46
|
const directory = node_fs_1.default.lstatSync(node_path_1.default.dirname(file));
|
|
46
|
-
if (!directory.isDirectory() || directory.isSymbolicLink() || (
|
|
47
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || !(0, secure_metadata_js_1.exactMode)(directory, 0o700)
|
|
47
48
|
|| (typeof process.getuid === "function" && directory.uid !== process.getuid()))
|
|
48
49
|
throw new Error(`${label} 目录不安全`);
|
|
49
50
|
const fd = node_fs_1.default.openSync(file, node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NOFOLLOW || 0));
|
|
50
51
|
try {
|
|
51
52
|
const stat = node_fs_1.default.fstatSync(fd);
|
|
52
|
-
if (!stat.isFile() || (
|
|
53
|
+
if (!stat.isFile() || !(0, secure_metadata_js_1.exactMode)(stat, 0o600)
|
|
53
54
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid()))
|
|
54
55
|
throw new Error(`${label} 文件不安全`);
|
|
55
56
|
return JSON.parse(node_fs_1.default.readFileSync(fd, "utf8"));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { exactMode } from "../platform/secure-metadata.mjs";
|
|
3
4
|
import { resolveOfficialLarkCli } from "./official-lark-cli.mjs";
|
|
4
5
|
export function larkChannelSourceConfigPath(agent) {
|
|
5
6
|
if (!agent.stateDir)
|
|
@@ -33,13 +34,13 @@ export function managedOfficialLarkCli(agent, env, resolve = resolveOfficialLark
|
|
|
33
34
|
}
|
|
34
35
|
function readPrivateJson(file, label) {
|
|
35
36
|
const directory = fs.lstatSync(path.dirname(file));
|
|
36
|
-
if (!directory.isDirectory() || directory.isSymbolicLink() || (directory
|
|
37
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || !exactMode(directory, 0o700)
|
|
37
38
|
|| (typeof process.getuid === "function" && directory.uid !== process.getuid()))
|
|
38
39
|
throw new Error(`${label} 目录不安全`);
|
|
39
40
|
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
40
41
|
try {
|
|
41
42
|
const stat = fs.fstatSync(fd);
|
|
42
|
-
if (!stat.isFile() || (stat
|
|
43
|
+
if (!stat.isFile() || !exactMode(stat, 0o600)
|
|
43
44
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid()))
|
|
44
45
|
throw new Error(`${label} 文件不安全`);
|
|
45
46
|
return JSON.parse(fs.readFileSync(fd, "utf8"));
|
package/dist/app/cli.mjs
CHANGED
|
@@ -111,7 +111,7 @@ if (!routes[command] && !wantsHelp) {
|
|
|
111
111
|
routes[command] = runtimeAgentAuthority ? ["lark-cli", command] : ["lark", command];
|
|
112
112
|
}
|
|
113
113
|
if (!routes[command]) {
|
|
114
|
-
console.log(`larkin — Run persistent-personality agents on Feishu
|
|
114
|
+
console.log(`larkin — Run persistent-personality agents on Feishu (Lark)
|
|
115
115
|
|
|
116
116
|
Usage: larkin <command>
|
|
117
117
|
setup Create or connect a bot, configure its Agent, start it, and open the dashboard
|
package/dist/app/lark-cli.mjs
CHANGED
|
@@ -269,6 +269,24 @@ function parseCommentReply(argv) {
|
|
|
269
269
|
throw new Error("comment reply 的 --text 超过 20000 字符");
|
|
270
270
|
return { messageId, text };
|
|
271
271
|
}
|
|
272
|
+
const IM_WRITE_MEMO_LIMIT = 512;
|
|
273
|
+
// 只标注、不拦截:每次成功写把「实际生效的幂等 key → 服务端返回的 message_id」记进备忘。
|
|
274
|
+
// 同 key 再次成功且服务端返回同一个 message_id,说明服务端走了幂等去重(没有产生新消息),
|
|
275
|
+
// 返回 true 供输出标注 duplicate。拦截权始终在服务端,备忘不会吞掉任何发送。
|
|
276
|
+
function recordImWriteMemo(store, key, messageId) {
|
|
277
|
+
let duplicate = false;
|
|
278
|
+
store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
279
|
+
state.im_write_memo ??= {};
|
|
280
|
+
const prior = state.im_write_memo[key];
|
|
281
|
+
if (prior && prior.message_id === messageId)
|
|
282
|
+
duplicate = true;
|
|
283
|
+
state.im_write_memo[key] = { message_id: messageId, updated_at: new Date().toISOString() };
|
|
284
|
+
const keys = Object.keys(state.im_write_memo);
|
|
285
|
+
for (const stale of keys.slice(0, Math.max(0, keys.length - IM_WRITE_MEMO_LIMIT)))
|
|
286
|
+
delete state.im_write_memo[stale];
|
|
287
|
+
});
|
|
288
|
+
return duplicate;
|
|
289
|
+
}
|
|
272
290
|
function runCommentReply(argv, privateEnv, io, dependencies, store) {
|
|
273
291
|
const input = parseCommentReply(argv);
|
|
274
292
|
const targetKey = store.resolveInboxMessageTarget(input.messageId);
|
|
@@ -499,9 +517,12 @@ function parseHistory(result, target, requireBotIdentity = false) {
|
|
|
499
517
|
}
|
|
500
518
|
return { messages };
|
|
501
519
|
}
|
|
502
|
-
|
|
520
|
+
// 防重编号只由 target + argv 决定:同一逻辑命令的重试永远得到同一个 key,
|
|
521
|
+
// 服务端幂等去重才能生效。cursor(freshness 水位)会随每次观察读/冲突合并推进,
|
|
522
|
+
// 掺入会让「已送达但回执丢失」的重试漂移成新 key,从而发出第二条消息。
|
|
523
|
+
function intentId(target, argv) {
|
|
503
524
|
const fingerprint = createHash("sha256")
|
|
504
|
-
.update(JSON.stringify([target,
|
|
525
|
+
.update(JSON.stringify([target, argv])).digest("hex");
|
|
505
526
|
return `larkin-${fingerprint.slice(0, 32)}`;
|
|
506
527
|
}
|
|
507
528
|
function emitFreshnessError(io, input) {
|
|
@@ -709,6 +730,26 @@ function emitNativeResult(result, io) {
|
|
|
709
730
|
}
|
|
710
731
|
return result.status ?? (result.signal ? portableSignalCode(result.signal) : 1);
|
|
711
732
|
}
|
|
733
|
+
function emitDuplicatedWrite(result, io, input) {
|
|
734
|
+
let providerResponse;
|
|
735
|
+
try {
|
|
736
|
+
providerResponse = JSON.parse(result.stdout || "");
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
providerResponse = { raw_stdout: result.stdout || "" };
|
|
740
|
+
}
|
|
741
|
+
const providerDocument = providerResponse && typeof providerResponse === "object" && !Array.isArray(providerResponse)
|
|
742
|
+
? providerResponse
|
|
743
|
+
: { provider_response: providerResponse };
|
|
744
|
+
io.stdout(`${JSON.stringify({
|
|
745
|
+
...providerDocument,
|
|
746
|
+
ok: true,
|
|
747
|
+
duplicate: true,
|
|
748
|
+
target: input.target,
|
|
749
|
+
...(result.stderr ? { provider_stderr_present: true } : {}),
|
|
750
|
+
})}\n`);
|
|
751
|
+
return 0;
|
|
752
|
+
}
|
|
712
753
|
function emitCommittedUnverified(result, io, input) {
|
|
713
754
|
let providerResponse;
|
|
714
755
|
try {
|
|
@@ -817,7 +858,16 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
817
858
|
store.mergeFreshnessCursor(targetKey, gated.current, mergeFeishuImCursor, generation);
|
|
818
859
|
return 3;
|
|
819
860
|
}
|
|
820
|
-
const
|
|
861
|
+
const intentKey = policyFlagValue(effectiveArgv, "--idempotency-key") ?? intentId(targetKey, effectiveArgv);
|
|
862
|
+
const write = callNative(botArgv(effectiveArgv, intentKey), privateEnv, io, nativeDependencies);
|
|
863
|
+
const writeMessage = writeResponseMessage(write);
|
|
864
|
+
const duplicate = !write.error && write.status === 0 && writeMessage
|
|
865
|
+
? recordImWriteMemo(store, intentKey, writeMessage.message_id) : false;
|
|
866
|
+
if (duplicate) {
|
|
867
|
+
observeSuccessfulWrite(write, target, targetKey, store, generation);
|
|
868
|
+
emitDuplicatedWrite(write, io, { target: targetKey });
|
|
869
|
+
return 0;
|
|
870
|
+
}
|
|
821
871
|
if (!write.error && write.status === 0 && !observeSuccessfulWrite(write, target, targetKey, store, generation)) {
|
|
822
872
|
const responseMessage = writeResponseMessage(write);
|
|
823
873
|
try {
|
|
@@ -7,11 +7,14 @@ import path from "node:path";
|
|
|
7
7
|
import { readProcessState } from "../platform/process-state.mjs";
|
|
8
8
|
import { currentProcessMetadata } from "../platform/process-inspect.cjs";
|
|
9
9
|
import { processCommandToken } from "./internal-command.mjs";
|
|
10
|
+
import { isWindows, notGroupOrWorldAccessible } from "../platform/secure-metadata.mjs";
|
|
10
11
|
const AGENT_ID = /^cli_[A-Za-z0-9]+$/;
|
|
11
12
|
const OPERATION_ID = /^[A-Za-z0-9_-]{8,128}$/;
|
|
12
13
|
const AUTHORIZATION = /^[A-Za-z0-9_-]{43,128}$/;
|
|
13
14
|
const UNIX_SOCKET_PATH_MAX_BYTES = process.platform === "darwin" ? 103 : 107;
|
|
14
15
|
function socketPathsFit(root) {
|
|
16
|
+
if (isWindows)
|
|
17
|
+
return true; // Windows named-pipe socket paths use a much larger limit
|
|
15
18
|
return ["supervisor.sock", "daemon.sock"].every((name) => Buffer.byteLength(path.join(root, name)) <= UNIX_SOCKET_PATH_MAX_BYTES);
|
|
16
19
|
}
|
|
17
20
|
function controlSocketRoot(larkinHome) {
|
|
@@ -21,6 +24,8 @@ function controlSocketRoot(larkinHome) {
|
|
|
21
24
|
const preferred = path.join(path.resolve(os.tmpdir()), leaf);
|
|
22
25
|
if (socketPathsFit(preferred))
|
|
23
26
|
return preferred;
|
|
27
|
+
if (isWindows)
|
|
28
|
+
throw new Error("无法生成满足 Windows socket 路径限制的 control root");
|
|
24
29
|
const fallback = path.join("/tmp", leaf);
|
|
25
30
|
if (socketPathsFit(fallback))
|
|
26
31
|
return fallback;
|
|
@@ -30,7 +35,7 @@ function assertSecureSocketDirectory(root) {
|
|
|
30
35
|
const stat = fs.lstatSync(root);
|
|
31
36
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
32
37
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
33
|
-
|| (stat
|
|
38
|
+
|| !notGroupOrWorldAccessible(stat))
|
|
34
39
|
throw new Error("control socket root 不安全");
|
|
35
40
|
return root;
|
|
36
41
|
}
|
|
@@ -52,7 +57,7 @@ function assertSecureRoot(root) {
|
|
|
52
57
|
const stat = fs.lstatSync(root);
|
|
53
58
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
54
59
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
55
|
-
|| (stat
|
|
60
|
+
|| !notGroupOrWorldAccessible(stat))
|
|
56
61
|
throw new Error("Larkin config root 必须由当前用户拥有且不可被其他用户访问");
|
|
57
62
|
}
|
|
58
63
|
function removeLegacyResetLedger(larkinHome) {
|
|
@@ -67,7 +72,7 @@ function removeLegacyResetLedger(larkinHome) {
|
|
|
67
72
|
throw error;
|
|
68
73
|
}
|
|
69
74
|
if (!stat.isFile() || stat.isSymbolicLink()
|
|
70
|
-
|| (typeof process.getuid === "function" && stat.uid !== process.getuid()) || (stat
|
|
75
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid()) || !notGroupOrWorldAccessible(stat)) {
|
|
71
76
|
throw new Error("legacy daemon control operation ledger 不安全");
|
|
72
77
|
}
|
|
73
78
|
fs.unlinkSync(file);
|
|
@@ -114,7 +119,7 @@ function secureAuthority(larkinHome) {
|
|
|
114
119
|
const stat = fs.lstatSync(file);
|
|
115
120
|
if (!stat.isFile() || stat.isSymbolicLink()
|
|
116
121
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
117
|
-
|| (stat
|
|
122
|
+
|| !notGroupOrWorldAccessible(stat))
|
|
118
123
|
throw new Error("daemon control authority 不安全");
|
|
119
124
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
120
125
|
const validBinding = (binding) => !!binding && Number.isSafeInteger(binding.pid) && binding.pid > 0
|
|
@@ -634,7 +639,7 @@ async function sendSupervisorRecovery(larkinHome, operationId, timeoutMs) {
|
|
|
634
639
|
const stat = fs.lstatSync(socket);
|
|
635
640
|
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
636
641
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
637
|
-
|| (stat
|
|
642
|
+
|| !notGroupOrWorldAccessible(stat))
|
|
638
643
|
throw new Error("supervisor control socket 不安全");
|
|
639
644
|
return await new Promise((resolve, reject) => {
|
|
640
645
|
const client = net.createConnection(socket);
|
|
@@ -690,7 +695,7 @@ async function requestAgentControl({ larkinHome, timeoutMs = 30_000, request, })
|
|
|
690
695
|
const stat = fs.lstatSync(socket);
|
|
691
696
|
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
692
697
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
693
|
-
|| (stat
|
|
698
|
+
|| !notGroupOrWorldAccessible(stat))
|
|
694
699
|
throw new Error("daemon control socket 不安全");
|
|
695
700
|
return await new Promise((resolve, reject) => {
|
|
696
701
|
const client = net.createConnection(socket);
|
|
@@ -12,10 +12,20 @@ exports.ensureOfficialLarkCliForSetup = ensureOfficialLarkCliForSetup;
|
|
|
12
12
|
const node_child_process_1 = require("node:child_process");
|
|
13
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
function safeRealpath(target) {
|
|
16
|
+
try {
|
|
17
|
+
return node_fs_1.default.realpathSync(target);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
15
23
|
exports.OFFICIAL_LARK_CLI_VERSION = "1.0.79";
|
|
16
24
|
exports.OFFICIAL_LARK_CLI_INSTALL = `npm install --global @larksuite/cli@${exports.OFFICIAL_LARK_CLI_VERSION}`;
|
|
17
25
|
function loginShellPath(dependencies) {
|
|
18
26
|
const env = dependencies.env ?? process.env;
|
|
27
|
+
if ((dependencies.platform ?? process.platform) === "win32")
|
|
28
|
+
return windowsLoginShellPath(dependencies, env);
|
|
19
29
|
const shell = dependencies.shell || env.SHELL || "/bin/sh";
|
|
20
30
|
const result = (dependencies.spawn ?? node_child_process_1.spawnSync)(shell, ["-lc", "command -v lark-cli 2>/dev/null"], {
|
|
21
31
|
encoding: "utf8", env,
|
|
@@ -25,6 +35,30 @@ function loginShellPath(dependencies) {
|
|
|
25
35
|
const candidate = String(result.stdout || "").split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "";
|
|
26
36
|
return node_path_1.default.isAbsolute(candidate) ? node_path_1.default.resolve(candidate) : null;
|
|
27
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Windows 登录 shell 探测:npm 全局安装会生成 `lark-cli.cmd`/`lark-cli` shim,真正的
|
|
40
|
+
* 原生二进制位于 `<npm 前缀>/node_modules/@larksuite/cli/bin/lark-cli.exe`。`where lark-cli`
|
|
41
|
+
* 返回 shim 路径,从 shim 所在目录推导原生二进制优先返回;否则回退到命中的 .exe/.cmd
|
|
42
|
+
* 路径(交由 officialPackage 校验)。
|
|
43
|
+
*/
|
|
44
|
+
function windowsLoginShellPath(dependencies, env) {
|
|
45
|
+
const shell = dependencies.shell || "cmd.exe";
|
|
46
|
+
const result = (dependencies.spawn ?? node_child_process_1.spawnSync)(shell, ["/d", "/s", "/c", "where lark-cli 2>nul"], {
|
|
47
|
+
encoding: "utf8", env,
|
|
48
|
+
});
|
|
49
|
+
if (result.status !== 0 || result.error)
|
|
50
|
+
return null;
|
|
51
|
+
for (const line of String(result.stdout || "").split(/\r?\n/).map((value) => value.trim()).filter(Boolean)) {
|
|
52
|
+
if (!node_path_1.default.isAbsolute(line))
|
|
53
|
+
continue;
|
|
54
|
+
const native = node_path_1.default.join(node_path_1.default.dirname(line), "node_modules", "@larksuite", "cli", "bin", "lark-cli.exe");
|
|
55
|
+
if (safeRealpath(native))
|
|
56
|
+
return node_path_1.default.resolve(native);
|
|
57
|
+
if (/\.(exe|cmd)$/i.test(line))
|
|
58
|
+
return node_path_1.default.resolve(line);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
28
62
|
function compatibleVersion(version) {
|
|
29
63
|
const parse = (value) => {
|
|
30
64
|
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
|
|
@@ -37,7 +71,7 @@ function compatibleVersion(version) {
|
|
|
37
71
|
return actual[0] > minimum[0] || (actual[0] === minimum[0]
|
|
38
72
|
&& (actual[1] > minimum[1] || (actual[1] === minimum[1] && actual[2] >= minimum[2])));
|
|
39
73
|
}
|
|
40
|
-
function officialPackage(executable) {
|
|
74
|
+
function officialPackage(executable, platform = process.platform) {
|
|
41
75
|
let resolved;
|
|
42
76
|
try {
|
|
43
77
|
resolved = node_fs_1.default.realpathSync(executable);
|
|
@@ -55,8 +89,11 @@ function officialPackage(executable) {
|
|
|
55
89
|
const packageRoot = node_fs_1.default.realpathSync(directory);
|
|
56
90
|
const requested = node_path_1.default.resolve(packageRoot, bin);
|
|
57
91
|
const relative = node_path_1.default.relative(packageRoot, requested);
|
|
58
|
-
|
|
59
|
-
|
|
92
|
+
const withinPackage = relative === "" || (!relative.startsWith(`..${node_path_1.default.sep}`) && relative !== "..");
|
|
93
|
+
const matchesBinTarget = withinPackage && safeRealpath(requested) === resolved;
|
|
94
|
+
const matchesNativeWindows = withinPackage && platform === "win32"
|
|
95
|
+
&& safeRealpath(node_path_1.default.join(packageRoot, "bin", "lark-cli.exe")) === resolved;
|
|
96
|
+
if (matchesBinTarget || matchesNativeWindows) {
|
|
60
97
|
return { command: node_path_1.default.resolve(executable), argsPrefix: [], version: manifest.version };
|
|
61
98
|
}
|
|
62
99
|
}
|
|
@@ -114,8 +151,29 @@ function validateOfficialCommand(command, env, spawn) {
|
|
|
114
151
|
* 仅当 PATH 解析出可用的官方 CLI 时才返回结果;否则返回 null 回退到
|
|
115
152
|
* 登录 shell 探测(保留对 shell 自定义 PATH 环境的兼容)。
|
|
116
153
|
*/
|
|
117
|
-
function probePathResolution(env) {
|
|
154
|
+
function probePathResolution(env, platform = process.platform) {
|
|
118
155
|
const dirs = String(env.PATH || "").split(node_path_1.default.delimiter).filter(Boolean);
|
|
156
|
+
if (platform === "win32") {
|
|
157
|
+
for (const dir of dirs) {
|
|
158
|
+
for (const candidate of [
|
|
159
|
+
node_path_1.default.resolve(dir, "node_modules", "@larksuite", "cli", "bin", "lark-cli.exe"),
|
|
160
|
+
node_path_1.default.resolve(dir, "lark-cli.exe"),
|
|
161
|
+
]) {
|
|
162
|
+
try {
|
|
163
|
+
if (!node_fs_1.default.statSync(candidate).isFile())
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const command = officialPackage(candidate, platform);
|
|
170
|
+
if (!command)
|
|
171
|
+
continue;
|
|
172
|
+
return validateOfficialCommand(command, env, node_child_process_1.spawnSync);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
119
177
|
for (const dir of dirs) {
|
|
120
178
|
const candidate = node_path_1.default.resolve(dir, "lark-cli");
|
|
121
179
|
try {
|
|
@@ -125,7 +183,7 @@ function probePathResolution(env) {
|
|
|
125
183
|
catch {
|
|
126
184
|
continue;
|
|
127
185
|
}
|
|
128
|
-
const command = officialPackage(candidate);
|
|
186
|
+
const command = officialPackage(candidate, platform);
|
|
129
187
|
if (!command)
|
|
130
188
|
return null;
|
|
131
189
|
return validateOfficialCommand(command, env, node_child_process_1.spawnSync);
|
|
@@ -134,8 +192,9 @@ function probePathResolution(env) {
|
|
|
134
192
|
}
|
|
135
193
|
function probeOfficialLarkCliUncached(dependencies) {
|
|
136
194
|
const env = dependencies.env ?? process.env;
|
|
195
|
+
const platform = dependencies.platform ?? process.platform;
|
|
137
196
|
if (!dependencies.spawn && !dependencies.shell) {
|
|
138
|
-
const fromPath = probePathResolution(env);
|
|
197
|
+
const fromPath = probePathResolution(env, platform);
|
|
139
198
|
if (fromPath)
|
|
140
199
|
return fromPath;
|
|
141
200
|
}
|
|
@@ -146,7 +205,7 @@ function probeOfficialLarkCliUncached(dependencies) {
|
|
|
146
205
|
reason: "真实 login shell 找不到官方 lark-cli",
|
|
147
206
|
nextAction: `运行 larkin setup,并确认执行:${exports.OFFICIAL_LARK_CLI_INSTALL}`,
|
|
148
207
|
};
|
|
149
|
-
const command = officialPackage(executable);
|
|
208
|
+
const command = officialPackage(executable, platform);
|
|
150
209
|
if (!command)
|
|
151
210
|
return {
|
|
152
211
|
state: "conflict",
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
function safeRealpath(target) {
|
|
5
|
+
try {
|
|
6
|
+
return fs.realpathSync(target);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
4
12
|
export const OFFICIAL_LARK_CLI_VERSION = "1.0.79";
|
|
5
13
|
export const OFFICIAL_LARK_CLI_INSTALL = `npm install --global @larksuite/cli@${OFFICIAL_LARK_CLI_VERSION}`;
|
|
6
14
|
function loginShellPath(dependencies) {
|
|
7
15
|
const env = dependencies.env ?? process.env;
|
|
16
|
+
if ((dependencies.platform ?? process.platform) === "win32")
|
|
17
|
+
return windowsLoginShellPath(dependencies, env);
|
|
8
18
|
const shell = dependencies.shell || env.SHELL || "/bin/sh";
|
|
9
19
|
const result = (dependencies.spawn ?? spawnSync)(shell, ["-lc", "command -v lark-cli 2>/dev/null"], {
|
|
10
20
|
encoding: "utf8", env,
|
|
@@ -14,6 +24,30 @@ function loginShellPath(dependencies) {
|
|
|
14
24
|
const candidate = String(result.stdout || "").split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "";
|
|
15
25
|
return path.isAbsolute(candidate) ? path.resolve(candidate) : null;
|
|
16
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Windows 登录 shell 探测:npm 全局安装会生成 `lark-cli.cmd`/`lark-cli` shim,真正的
|
|
29
|
+
* 原生二进制位于 `<npm 前缀>/node_modules/@larksuite/cli/bin/lark-cli.exe`。`where lark-cli`
|
|
30
|
+
* 返回 shim 路径,从 shim 所在目录推导原生二进制优先返回;否则回退到命中的 .exe/.cmd
|
|
31
|
+
* 路径(交由 officialPackage 校验)。
|
|
32
|
+
*/
|
|
33
|
+
function windowsLoginShellPath(dependencies, env) {
|
|
34
|
+
const shell = dependencies.shell || "cmd.exe";
|
|
35
|
+
const result = (dependencies.spawn ?? spawnSync)(shell, ["/d", "/s", "/c", "where lark-cli 2>nul"], {
|
|
36
|
+
encoding: "utf8", env,
|
|
37
|
+
});
|
|
38
|
+
if (result.status !== 0 || result.error)
|
|
39
|
+
return null;
|
|
40
|
+
for (const line of String(result.stdout || "").split(/\r?\n/).map((value) => value.trim()).filter(Boolean)) {
|
|
41
|
+
if (!path.isAbsolute(line))
|
|
42
|
+
continue;
|
|
43
|
+
const native = path.join(path.dirname(line), "node_modules", "@larksuite", "cli", "bin", "lark-cli.exe");
|
|
44
|
+
if (safeRealpath(native))
|
|
45
|
+
return path.resolve(native);
|
|
46
|
+
if (/\.(exe|cmd)$/i.test(line))
|
|
47
|
+
return path.resolve(line);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
17
51
|
function compatibleVersion(version) {
|
|
18
52
|
const parse = (value) => {
|
|
19
53
|
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
|
|
@@ -26,7 +60,7 @@ function compatibleVersion(version) {
|
|
|
26
60
|
return actual[0] > minimum[0] || (actual[0] === minimum[0]
|
|
27
61
|
&& (actual[1] > minimum[1] || (actual[1] === minimum[1] && actual[2] >= minimum[2])));
|
|
28
62
|
}
|
|
29
|
-
function officialPackage(executable) {
|
|
63
|
+
function officialPackage(executable, platform = process.platform) {
|
|
30
64
|
let resolved;
|
|
31
65
|
try {
|
|
32
66
|
resolved = fs.realpathSync(executable);
|
|
@@ -44,8 +78,11 @@ function officialPackage(executable) {
|
|
|
44
78
|
const packageRoot = fs.realpathSync(directory);
|
|
45
79
|
const requested = path.resolve(packageRoot, bin);
|
|
46
80
|
const relative = path.relative(packageRoot, requested);
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
const withinPackage = relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..");
|
|
82
|
+
const matchesBinTarget = withinPackage && safeRealpath(requested) === resolved;
|
|
83
|
+
const matchesNativeWindows = withinPackage && platform === "win32"
|
|
84
|
+
&& safeRealpath(path.join(packageRoot, "bin", "lark-cli.exe")) === resolved;
|
|
85
|
+
if (matchesBinTarget || matchesNativeWindows) {
|
|
49
86
|
return { command: path.resolve(executable), argsPrefix: [], version: manifest.version };
|
|
50
87
|
}
|
|
51
88
|
}
|
|
@@ -103,8 +140,29 @@ function validateOfficialCommand(command, env, spawn) {
|
|
|
103
140
|
* 仅当 PATH 解析出可用的官方 CLI 时才返回结果;否则返回 null 回退到
|
|
104
141
|
* 登录 shell 探测(保留对 shell 自定义 PATH 环境的兼容)。
|
|
105
142
|
*/
|
|
106
|
-
function probePathResolution(env) {
|
|
143
|
+
function probePathResolution(env, platform = process.platform) {
|
|
107
144
|
const dirs = String(env.PATH || "").split(path.delimiter).filter(Boolean);
|
|
145
|
+
if (platform === "win32") {
|
|
146
|
+
for (const dir of dirs) {
|
|
147
|
+
for (const candidate of [
|
|
148
|
+
path.resolve(dir, "node_modules", "@larksuite", "cli", "bin", "lark-cli.exe"),
|
|
149
|
+
path.resolve(dir, "lark-cli.exe"),
|
|
150
|
+
]) {
|
|
151
|
+
try {
|
|
152
|
+
if (!fs.statSync(candidate).isFile())
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const command = officialPackage(candidate, platform);
|
|
159
|
+
if (!command)
|
|
160
|
+
continue;
|
|
161
|
+
return validateOfficialCommand(command, env, spawnSync);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
108
166
|
for (const dir of dirs) {
|
|
109
167
|
const candidate = path.resolve(dir, "lark-cli");
|
|
110
168
|
try {
|
|
@@ -114,7 +172,7 @@ function probePathResolution(env) {
|
|
|
114
172
|
catch {
|
|
115
173
|
continue;
|
|
116
174
|
}
|
|
117
|
-
const command = officialPackage(candidate);
|
|
175
|
+
const command = officialPackage(candidate, platform);
|
|
118
176
|
if (!command)
|
|
119
177
|
return null;
|
|
120
178
|
return validateOfficialCommand(command, env, spawnSync);
|
|
@@ -123,8 +181,9 @@ function probePathResolution(env) {
|
|
|
123
181
|
}
|
|
124
182
|
function probeOfficialLarkCliUncached(dependencies) {
|
|
125
183
|
const env = dependencies.env ?? process.env;
|
|
184
|
+
const platform = dependencies.platform ?? process.platform;
|
|
126
185
|
if (!dependencies.spawn && !dependencies.shell) {
|
|
127
|
-
const fromPath = probePathResolution(env);
|
|
186
|
+
const fromPath = probePathResolution(env, platform);
|
|
128
187
|
if (fromPath)
|
|
129
188
|
return fromPath;
|
|
130
189
|
}
|
|
@@ -135,7 +194,7 @@ function probeOfficialLarkCliUncached(dependencies) {
|
|
|
135
194
|
reason: "真实 login shell 找不到官方 lark-cli",
|
|
136
195
|
nextAction: `运行 larkin setup,并确认执行:${OFFICIAL_LARK_CLI_INSTALL}`,
|
|
137
196
|
};
|
|
138
|
-
const command = officialPackage(executable);
|
|
197
|
+
const command = officialPackage(executable, platform);
|
|
139
198
|
if (!command)
|
|
140
199
|
return {
|
|
141
200
|
state: "conflict",
|
|
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { exactMode, notGroupOrWorldAccessible } from "../platform/secure-metadata.mjs";
|
|
6
7
|
import { acquireProcessLock } from "../platform/process-state.mjs";
|
|
7
8
|
import { assertSecureBotsDirectory, readSecureBotCredential, validCredentialRecord, } from "../setup/run-credential-preflight.mjs";
|
|
8
9
|
import { resolveOfficialLarkCli } from "./official-lark-cli.mjs";
|
|
@@ -12,7 +13,7 @@ function assertSecureProfileDirectory(directory) {
|
|
|
12
13
|
const stat = fs.lstatSync(directory);
|
|
13
14
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
14
15
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
15
|
-
|| (stat
|
|
16
|
+
|| !exactMode(stat, 0o700))
|
|
16
17
|
throw new Error("lark-cli profile 目录不安全");
|
|
17
18
|
}
|
|
18
19
|
function captureProfileSnapshot(file) {
|
|
@@ -22,7 +23,7 @@ function captureProfileSnapshot(file) {
|
|
|
22
23
|
const stat = fs.fstatSync(fd);
|
|
23
24
|
if (!stat.isFile()
|
|
24
25
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
25
|
-
|| (stat
|
|
26
|
+
|| !notGroupOrWorldAccessible(stat))
|
|
26
27
|
throw new Error("lark-cli config.json 不安全");
|
|
27
28
|
const raw = fs.readFileSync(fd);
|
|
28
29
|
const value = JSON.parse(raw.toString("utf8"));
|
package/dist/app/setup.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const flag = (name) => { const index = argv.indexOf(name); return index >= 0 ? a
|
|
|
22
22
|
const say = (...args) => console.log(...args);
|
|
23
23
|
const die = (message) => { console.error(`✗ ${message}`); process.exit(1); };
|
|
24
24
|
if (argv.some((arg) => arg === "--app-id" || arg.startsWith("--app-id="))) {
|
|
25
|
-
die("setup 不支持 --app-id;请运行 larkin setup
|
|
25
|
+
die("setup 不支持 --app-id;请运行 larkin setup 并在飞书(Lark)网页中选择机器人");
|
|
26
26
|
}
|
|
27
27
|
if (argv.some((arg) => arg === "--no-dashboard" || arg.startsWith("--no-dashboard="))) {
|
|
28
28
|
die("--no-dashboard 已移除;dashboard 由 larkin start 统一管理");
|
|
@@ -67,7 +67,7 @@ if (!OPT.help && normalizedRuntime.distribution === "external" && (flag("--provi
|
|
|
67
67
|
die("external-pi 使用已有 pi 环境,不接受 --provider/--api-key/--base-url");
|
|
68
68
|
}
|
|
69
69
|
if (OPT.help) {
|
|
70
|
-
say(`larkin setup — Create or connect a Feishu bot, then configure and attach its Agent
|
|
70
|
+
say(`larkin setup — Create or connect a Feishu (Lark) bot, then configure and attach its Agent
|
|
71
71
|
|
|
72
72
|
Usage:
|
|
73
73
|
larkin setup Select a bot in the browser and run setup(默认:内置 Pi + 配置完成即热挂载)
|
|
@@ -128,7 +128,7 @@ export async function main() {
|
|
|
128
128
|
env: process.env,
|
|
129
129
|
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
130
130
|
async confirmInstall(command) {
|
|
131
|
-
say("[setup 0/5] Larkin 需要未修改的官方 lark-cli 作为 Feishu 命令下游。");
|
|
131
|
+
say("[setup 0/5] Larkin 需要未修改的官方 lark-cli 作为 Feishu (Lark) 命令下游。");
|
|
132
132
|
say(`将执行:${command}`);
|
|
133
133
|
const input = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
134
134
|
const answer = (await input.question("是否安装?[y/N] ")).trim().toLowerCase();
|
|
@@ -137,7 +137,7 @@ export async function main() {
|
|
|
137
137
|
},
|
|
138
138
|
}).catch((error) => die(error instanceof Error ? error.message : String(error)));
|
|
139
139
|
say(`[setup 0/5] ✓ 官方 lark-cli ${official.command.version}: ${official.command.command}${official.installed ? "(刚刚安装)" : ""}`);
|
|
140
|
-
say("\nAgent
|
|
140
|
+
say("\nAgent 与飞书(Lark)机器人按 App ID 一一对应:");
|
|
141
141
|
say(" • 网页选择同一个机器人 → 热更新该 Agent,不重启其他 Agent");
|
|
142
142
|
say(" • 网页创建新机器人 → 热挂载新 Agent,状态彼此独立\n");
|
|
143
143
|
fs.mkdirSync(CFG_DIR, { recursive: true, mode: 0o700 });
|