larkin 0.4.8 → 0.4.9
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 +2 -0
- package/dist/agent/context-prompt.cjs +2 -1
- package/dist/agent/context-prompt.mjs +2 -1
- package/dist/app/cli.mjs +2 -2
- package/dist/app/runtime-agent-config.mjs +7 -3
- package/dist/app/setup.mjs +11 -0
- package/dist/feishu/host-shell.cjs +4 -3
- package/dist/feishu/host-shell.mjs +4 -3
- package/dist/feishu/platform-hosts.cjs +49 -0
- package/dist/feishu/platform-hosts.mjs +40 -0
- package/dist/setup/bot-register.mjs +50 -4
- package/dist/setup/grant-scopes.mjs +16 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,8 @@ Feishu clients do not reliably render Markdown links such as `[label](URL)` as c
|
|
|
87
87
|
|
|
88
88
|
## Setup and configuration
|
|
89
89
|
|
|
90
|
+
Feishu (https://open.feishu.cn) and Lark (https://open.larksuite.com) are different platforms; `larkin setup` must be told which brand with `--tenant feishu|lark` or the interactive prompt before the authorization QR, and must never emit a `feishu.cn` host for a Lark tenant.
|
|
91
|
+
|
|
90
92
|
During setup, a new Agent is offered Pi first, followed by Codex and Claude Code. Provider keys are stored only in the selected Agent's private provider directory, not in the ordinary Agent config. Setup also discovers every API-key and OAuth/subscription login exposed by the pinned official Pi registry and delegates those flows to Pi.
|
|
91
93
|
|
|
92
94
|
Use `larkin pi-auth status` or `larkin pi-auth logout <provider>` to manage Pi auth. The builtin Pi runtime loads Larkin's two supported extensions inline: background subagents and the 60-second foreground bash timeout guard are enabled without writing extension files or arguments. A compatible external Pi installation keeps the existing explicit `-e` extension path.
|
|
@@ -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-v21";
|
|
20
20
|
/**
|
|
21
21
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
22
22
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -126,6 +126,7 @@ class ContextPromptBuilder {
|
|
|
126
126
|
"## Feishu IM command map",
|
|
127
127
|
"",
|
|
128
128
|
`Use only the Larkin-owned \`${executable}\` command for Feishu. Bot identity, private configuration, and freshness are Runtime-bound; the wrapper delegates to the unmodified global official CLI. Use \`${executable} <command> --help\`, never invoke bare \`lark-cli\`, and never pass \`--agent\`, \`--as user\`, \`--profile\`, or \`--config-dir\`. If a command reports a missing scope, relay that error unchanged and ask the user to authorize it; do not bypass the scope boundary.`,
|
|
129
|
+
"If a platform URL must be shown, use the current Agent tenant host; never emit feishu.cn for a Lark tenant.",
|
|
129
130
|
`Use the exact Inbox target for history reads. For \`thread:<chat_id>:<thread_id>\`, run \`${executable} im +threads-messages-list --thread <thread_id> --order desc --page-size 10 --no-reactions --json\`. For \`chat:<chat_id>\`, run \`${executable} im +chat-messages-list --chat-id <chat_id> --order desc --page-size 10 --no-reactions --json\`.`,
|
|
130
131
|
"Successful history response messages are always at `data.messages`. Never use a chat-wide fallback for a thread target, never merge stderr with `2>&1` before parsing JSON, and never truncate structured output before parsing it.",
|
|
131
132
|
"If the scoped history read fails or its schema is invalid, fail visibly. Do not reuse remembered or hard-coded text to make the task appear successful.",
|
|
@@ -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-v21";
|
|
17
17
|
/**
|
|
18
18
|
* Agent 间协作唤醒引导(issue #75):纯文本 @ 不会产生飞书 mention 事件,
|
|
19
19
|
* 必须由模型直接构造真实的 mention 元素;`--mention` 扩展参数已移除。
|
|
@@ -123,6 +123,7 @@ export class ContextPromptBuilder {
|
|
|
123
123
|
"## Feishu IM command map",
|
|
124
124
|
"",
|
|
125
125
|
`Use only the Larkin-owned \`${executable}\` command for Feishu. Bot identity, private configuration, and freshness are Runtime-bound; the wrapper delegates to the unmodified global official CLI. Use \`${executable} <command> --help\`, never invoke bare \`lark-cli\`, and never pass \`--agent\`, \`--as user\`, \`--profile\`, or \`--config-dir\`. If a command reports a missing scope, relay that error unchanged and ask the user to authorize it; do not bypass the scope boundary.`,
|
|
126
|
+
"If a platform URL must be shown, use the current Agent tenant host; never emit feishu.cn for a Lark tenant.",
|
|
126
127
|
`Use the exact Inbox target for history reads. For \`thread:<chat_id>:<thread_id>\`, run \`${executable} im +threads-messages-list --thread <thread_id> --order desc --page-size 10 --no-reactions --json\`. For \`chat:<chat_id>\`, run \`${executable} im +chat-messages-list --chat-id <chat_id> --order desc --page-size 10 --no-reactions --json\`.`,
|
|
127
128
|
"Successful history response messages are always at `data.messages`. Never use a chat-wide fallback for a thread target, never merge stderr with `2>&1` before parsing JSON, and never truncate structured output before parsing it.",
|
|
128
129
|
"If the scoped history read fails or its schema is invalid, fail visibly. Do not reuse remembered or hard-coded text to make the task appear successful.",
|
package/dist/app/cli.mjs
CHANGED
|
@@ -49,8 +49,8 @@ if (runtimeAgentAuthority && command === "comment")
|
|
|
49
49
|
const commandHelp = {
|
|
50
50
|
start: `Usage: larkin start [--agent <App ID> | --agents <App ID,...>]
|
|
51
51
|
Start one foreground supervisor for the daemon and local dashboard, or reuse it.`,
|
|
52
|
-
setup: `Usage: larkin setup [--runtime <builtin-pi|external-pi|codex|claude>] [--provider <id> --api-key <key>]
|
|
53
|
-
Run setup to create or connect a bot, configure its Agent, and attach it. Interactive terminals keep the guided flow; Agent-driven (non-TTY) runs default to builtin-pi and need --provider/--api-key (or --runtime external-pi).`,
|
|
52
|
+
setup: `Usage: larkin setup [--tenant feishu|lark] [--runtime <builtin-pi|external-pi|codex|claude>] [--provider <id> --api-key <key>]
|
|
53
|
+
Run setup to create or connect a bot, configure its Agent, and attach it. Choose Feishu or Lark before the authorization QR (--tenant; default feishu). Interactive terminals keep the guided flow; Agent-driven (non-TTY) runs default to builtin-pi and need --provider/--api-key (or --runtime external-pi).`,
|
|
54
54
|
status: `Usage: larkin status [--json]
|
|
55
55
|
Show Agent configuration, bot identity, credentials, and connection status. Use --json for readiness automation.`,
|
|
56
56
|
agents: `Usage: larkin agents [--json]
|
|
@@ -9,6 +9,7 @@ import { assertSecureBotsDirectory, readSecureBotCredential, validCredentialReco
|
|
|
9
9
|
import { resolveOfficialLarkCli } from "./official-lark-cli.mjs";
|
|
10
10
|
import { internalCommandSpec } from "./internal-command.mjs";
|
|
11
11
|
import { assertAgentWorkspaceBound, larkChannelSourceConfigPath, larkChannelWorkspaceConfigPath, managedLarkCliEnv, } from "./agent-lark-cli-workspace.mjs";
|
|
12
|
+
import { accountTenantFromOpenHost, openPlatformHost, } from "../feishu/platform-hosts.mjs";
|
|
12
13
|
function assertSecureProfileDirectory(directory) {
|
|
13
14
|
const stat = fs.lstatSync(directory);
|
|
14
15
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
@@ -140,7 +141,7 @@ export function hydrateRuntimeAgent(configDir, agent) {
|
|
|
140
141
|
return {
|
|
141
142
|
...agent,
|
|
142
143
|
feishuAppSecret: credential.appSecret,
|
|
143
|
-
feishuDomain: credential.tenant
|
|
144
|
+
feishuDomain: openPlatformHost(credential.tenant),
|
|
144
145
|
credentialRevision,
|
|
145
146
|
};
|
|
146
147
|
}
|
|
@@ -248,7 +249,7 @@ export function installRuntimeCommandShims(agent) {
|
|
|
248
249
|
}
|
|
249
250
|
return commandDir;
|
|
250
251
|
}
|
|
251
|
-
function sourceProjection(agent, env) {
|
|
252
|
+
export function sourceProjection(agent, env) {
|
|
252
253
|
const configDir = path.resolve(env.LARKIN_CONFIG_DIR || "");
|
|
253
254
|
if (!configDir)
|
|
254
255
|
throw new Error("LARKIN_CONFIG_DIR required for lark-channel source projection");
|
|
@@ -261,7 +262,7 @@ function sourceProjection(agent, env) {
|
|
|
261
262
|
accounts: { app: {
|
|
262
263
|
id: agent.feishuAppId,
|
|
263
264
|
secret: { source: "exec", provider: "larkin-bot-credential", id: agent.feishuAppId },
|
|
264
|
-
tenant: agent.feishuDomain
|
|
265
|
+
tenant: accountTenantFromOpenHost(agent.feishuDomain),
|
|
265
266
|
} },
|
|
266
267
|
secrets: { providers: { "larkin-bot-credential": {
|
|
267
268
|
source: "exec", command: provider.command, args: provider.args,
|
|
@@ -294,6 +295,9 @@ function validateSourceProjection(file, agent) {
|
|
|
294
295
|
throw new Error("lark-channel source projection 不得包含 plaintext secret 字段");
|
|
295
296
|
if (root.credentialRevision !== agent.credentialRevision)
|
|
296
297
|
throw new Error(`Agent ${agent.agentId} lark-channel credential revision mismatch`);
|
|
298
|
+
if (app?.tenant !== accountTenantFromOpenHost(agent.feishuDomain)) {
|
|
299
|
+
throw new Error(`Agent ${agent.agentId} lark-channel source projection tenant mismatch`);
|
|
300
|
+
}
|
|
297
301
|
}
|
|
298
302
|
export function syncAgentProfile(agent, env, dependencies = {}) {
|
|
299
303
|
const expected = path.join(path.resolve(env.LARKIN_CONFIG_DIR || ""), "state", "agents", agent.agentId, "lark-cli-config");
|
package/dist/app/setup.mjs
CHANGED
|
@@ -82,6 +82,11 @@ Options:
|
|
|
82
82
|
--base-url <url> custom 端点(provider=custom 时必填;也可覆盖 preset 默认端点)
|
|
83
83
|
--model <id> 模型 ID;默认取 provider 预设。不同 runtime 的模型可用
|
|
84
84
|
\`larkin model\` 查看 / 切换
|
|
85
|
+
--tenant feishu|lark 授权二维码之前选择品牌。默认 feishu(accounts.feishu.cn /
|
|
86
|
+
https://open.feishu.cn)。Lark 用 --tenant lark
|
|
87
|
+
(accounts.larksuite.com / https://open.larksuite.com)。
|
|
88
|
+
交互式未指定时会先询问。飞书与 Lark 是不同平台,
|
|
89
|
+
Lark 租户不得使用 feishu.cn 主机。
|
|
85
90
|
|
|
86
91
|
setup handles browser authorization, permission grants, credential storage, Agent configuration,
|
|
87
92
|
and target-only hot attach. Each Agent is identified by its bot App ID: selecting the same bot reuses
|
|
@@ -152,6 +157,12 @@ export async function main() {
|
|
|
152
157
|
registerArgs.push("--pi-distribution", normalizedRuntime.distribution);
|
|
153
158
|
}
|
|
154
159
|
registerArgs.push("--comment-subscription", OPT.commentSubscription);
|
|
160
|
+
if (has("--tenant")) {
|
|
161
|
+
const tenant = flag("--tenant");
|
|
162
|
+
if (tenant !== "feishu" && tenant !== "lark")
|
|
163
|
+
die("--tenant 只支持 feishu 或 lark");
|
|
164
|
+
registerArgs.push("--tenant", tenant === "lark" ? "lark" : "feishu");
|
|
165
|
+
}
|
|
155
166
|
for (const name of ["--provider", "--api-key", "--base-url", "--model"]) {
|
|
156
167
|
const value = flag(name);
|
|
157
168
|
if (value)
|
|
@@ -61,6 +61,7 @@ const internal_command_js_1 = require("../app/internal-command.cjs");
|
|
|
61
61
|
const agent_lark_cli_workspace_js_1 = require("../app/agent-lark-cli-workspace.cjs");
|
|
62
62
|
const agent_readiness_js_1 = require("../app/agent-readiness.cjs");
|
|
63
63
|
const document_comment_js_1 = require("./document-comment.cjs");
|
|
64
|
+
const platform_hosts_js_1 = require("./platform-hosts.cjs");
|
|
64
65
|
const PENDING_DOCUMENT_COMMENT_LIMIT = 256;
|
|
65
66
|
function isRecord(value) {
|
|
66
67
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -152,7 +153,7 @@ function loadAgents(env, testEventSource, larkinHome, reconcileAgentWorkspaceImp
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
if (!testEventSource && (typeof candidate.feishuAppSecret !== "string" || !candidate.feishuAppSecret.trim()
|
|
155
|
-
|| !
|
|
156
|
+
|| !(0, platform_hosts_js_1.isOpenPlatformHost)(candidate.feishuDomain))) {
|
|
156
157
|
throw new Error(`LARKIN_AGENTS_CONFIG Agent ${id} 缺少有效 channel 凭证/domain`);
|
|
157
158
|
}
|
|
158
159
|
const expectedWorkspace = node_path_1.default.join(larkinHome, "agents", id);
|
|
@@ -742,7 +743,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
742
743
|
const channel = createLarkChannel({
|
|
743
744
|
appId: agent.feishuAppId,
|
|
744
745
|
appSecret: agent.feishuAppSecret,
|
|
745
|
-
domain: agent.feishuDomain
|
|
746
|
+
domain: (0, platform_hosts_js_1.requireOpenDomain)(agent.feishuDomain),
|
|
746
747
|
source: "larkin",
|
|
747
748
|
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
748
749
|
// issue #88:关闭 SDK 的防抖批量合并(默认 600ms),逐条投递,
|
|
@@ -857,7 +858,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
857
858
|
const channel = createLarkChannel({
|
|
858
859
|
appId: agent.feishuAppId,
|
|
859
860
|
appSecret: agent.feishuAppSecret,
|
|
860
|
-
domain: agent.feishuDomain
|
|
861
|
+
domain: (0, platform_hosts_js_1.requireOpenDomain)(agent.feishuDomain),
|
|
861
862
|
source: "larkin",
|
|
862
863
|
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
863
864
|
// issue #88:关闭 SDK 的防抖批量合并(默认 600ms),逐条投递,
|
|
@@ -21,6 +21,7 @@ import { processCommandToken } from "../app/internal-command.mjs";
|
|
|
21
21
|
import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
|
|
22
22
|
import { isChannelReconnecting, isRuntimeReadinessCurrent } from "../app/agent-readiness.mjs";
|
|
23
23
|
import { documentCommentMessageId, documentCommentNoticeType, projectDocumentCommentEnvelope, resolveDocumentCommentContext, } from "./document-comment.mjs";
|
|
24
|
+
import { isOpenPlatformHost, requireOpenDomain } from "./platform-hosts.mjs";
|
|
24
25
|
const PENDING_DOCUMENT_COMMENT_LIMIT = 256;
|
|
25
26
|
function isRecord(value) {
|
|
26
27
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -112,7 +113,7 @@ function loadAgents(env, testEventSource, larkinHome, reconcileAgentWorkspaceImp
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
if (!testEventSource && (typeof candidate.feishuAppSecret !== "string" || !candidate.feishuAppSecret.trim()
|
|
115
|
-
|| !
|
|
116
|
+
|| !isOpenPlatformHost(candidate.feishuDomain))) {
|
|
116
117
|
throw new Error(`LARKIN_AGENTS_CONFIG Agent ${id} 缺少有效 channel 凭证/domain`);
|
|
117
118
|
}
|
|
118
119
|
const expectedWorkspace = path.join(larkinHome, "agents", id);
|
|
@@ -702,7 +703,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
702
703
|
const channel = createLarkChannel({
|
|
703
704
|
appId: agent.feishuAppId,
|
|
704
705
|
appSecret: agent.feishuAppSecret,
|
|
705
|
-
domain: agent.feishuDomain
|
|
706
|
+
domain: requireOpenDomain(agent.feishuDomain),
|
|
706
707
|
source: "larkin",
|
|
707
708
|
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
708
709
|
// issue #88:关闭 SDK 的防抖批量合并(默认 600ms),逐条投递,
|
|
@@ -817,7 +818,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
817
818
|
const channel = createLarkChannel({
|
|
818
819
|
appId: agent.feishuAppId,
|
|
819
820
|
appSecret: agent.feishuAppSecret,
|
|
820
|
-
domain: agent.feishuDomain
|
|
821
|
+
domain: requireOpenDomain(agent.feishuDomain),
|
|
821
822
|
source: "larkin",
|
|
822
823
|
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
823
824
|
// issue #88:关闭 SDK 的防抖批量合并(默认 600ms),逐条投递,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Official Feishu vs Lark hosts from @larksuite/cli ResolveEndpoints
|
|
3
|
+
// (open / accounts / applink) plus Open-host docs and console paths.
|
|
4
|
+
// Do not add larkoffice.com or any unofficial host.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PLATFORM_HOSTS = void 0;
|
|
7
|
+
exports.parseLarkinTenant = parseLarkinTenant;
|
|
8
|
+
exports.openPlatformHost = openPlatformHost;
|
|
9
|
+
exports.registerAppAccountsHost = registerAppAccountsHost;
|
|
10
|
+
exports.isOpenPlatformHost = isOpenPlatformHost;
|
|
11
|
+
exports.requireOpenDomain = requireOpenDomain;
|
|
12
|
+
exports.accountTenantFromOpenHost = accountTenantFromOpenHost;
|
|
13
|
+
exports.PLATFORM_HOSTS = {
|
|
14
|
+
feishu: {
|
|
15
|
+
open: "https://open.feishu.cn",
|
|
16
|
+
accounts: "https://accounts.feishu.cn",
|
|
17
|
+
applink: "https://applink.feishu.cn",
|
|
18
|
+
docs: "https://open.feishu.cn/document",
|
|
19
|
+
console: "https://open.feishu.cn/app",
|
|
20
|
+
},
|
|
21
|
+
lark: {
|
|
22
|
+
open: "https://open.larksuite.com",
|
|
23
|
+
accounts: "https://accounts.larksuite.com",
|
|
24
|
+
applink: "https://applink.larksuite.com",
|
|
25
|
+
docs: "https://open.larksuite.com/document",
|
|
26
|
+
console: "https://open.larksuite.com/app",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
function parseLarkinTenant(value) {
|
|
30
|
+
return value === "feishu" || value === "lark" ? value : null;
|
|
31
|
+
}
|
|
32
|
+
function openPlatformHost(tenant) {
|
|
33
|
+
return exports.PLATFORM_HOSTS[tenant].open;
|
|
34
|
+
}
|
|
35
|
+
function registerAppAccountsHost(tenant) {
|
|
36
|
+
// node-sdk registerApp.domain / larkDomain interpolate https://${host}
|
|
37
|
+
return new URL(exports.PLATFORM_HOSTS[tenant].accounts).host;
|
|
38
|
+
}
|
|
39
|
+
function isOpenPlatformHost(value) {
|
|
40
|
+
return value === exports.PLATFORM_HOSTS.feishu.open || value === exports.PLATFORM_HOSTS.lark.open;
|
|
41
|
+
}
|
|
42
|
+
function requireOpenDomain(value) {
|
|
43
|
+
if (isOpenPlatformHost(value))
|
|
44
|
+
return value;
|
|
45
|
+
throw new Error("missing or invalid Open Platform domain");
|
|
46
|
+
}
|
|
47
|
+
function accountTenantFromOpenHost(domain) {
|
|
48
|
+
return domain === exports.PLATFORM_HOSTS.lark.open ? "lark" : "feishu";
|
|
49
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Official Feishu vs Lark hosts from @larksuite/cli ResolveEndpoints
|
|
2
|
+
// (open / accounts / applink) plus Open-host docs and console paths.
|
|
3
|
+
// Do not add larkoffice.com or any unofficial host.
|
|
4
|
+
export const PLATFORM_HOSTS = {
|
|
5
|
+
feishu: {
|
|
6
|
+
open: "https://open.feishu.cn",
|
|
7
|
+
accounts: "https://accounts.feishu.cn",
|
|
8
|
+
applink: "https://applink.feishu.cn",
|
|
9
|
+
docs: "https://open.feishu.cn/document",
|
|
10
|
+
console: "https://open.feishu.cn/app",
|
|
11
|
+
},
|
|
12
|
+
lark: {
|
|
13
|
+
open: "https://open.larksuite.com",
|
|
14
|
+
accounts: "https://accounts.larksuite.com",
|
|
15
|
+
applink: "https://applink.larksuite.com",
|
|
16
|
+
docs: "https://open.larksuite.com/document",
|
|
17
|
+
console: "https://open.larksuite.com/app",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export function parseLarkinTenant(value) {
|
|
21
|
+
return value === "feishu" || value === "lark" ? value : null;
|
|
22
|
+
}
|
|
23
|
+
export function openPlatformHost(tenant) {
|
|
24
|
+
return PLATFORM_HOSTS[tenant].open;
|
|
25
|
+
}
|
|
26
|
+
export function registerAppAccountsHost(tenant) {
|
|
27
|
+
// node-sdk registerApp.domain / larkDomain interpolate https://${host}
|
|
28
|
+
return new URL(PLATFORM_HOSTS[tenant].accounts).host;
|
|
29
|
+
}
|
|
30
|
+
export function isOpenPlatformHost(value) {
|
|
31
|
+
return value === PLATFORM_HOSTS.feishu.open || value === PLATFORM_HOSTS.lark.open;
|
|
32
|
+
}
|
|
33
|
+
export function requireOpenDomain(value) {
|
|
34
|
+
if (isOpenPlatformHost(value))
|
|
35
|
+
return value;
|
|
36
|
+
throw new Error("missing or invalid Open Platform domain");
|
|
37
|
+
}
|
|
38
|
+
export function accountTenantFromOpenHost(domain) {
|
|
39
|
+
return domain === PLATFORM_HOSTS.lark.open ? "lark" : "feishu";
|
|
40
|
+
}
|
|
@@ -17,6 +17,7 @@ import { probeNativeRuntimeReadiness } from "../runtime/runtime-readiness.mjs";
|
|
|
17
17
|
import { configureBuiltinPiProviderModel, stageBuiltinPiProvider, validatePiBaseUrl, validateBuiltinPiProviderSelection, listProviderModels, PI_PROVIDER_PRESETS } from "../runtime/pi-provider-config.mjs";
|
|
18
18
|
import { beginBuiltinPiCredentialTransaction, createOfficialPiCredentialRuntime, createOfficialPiAuthInteraction, createOfficialPiModelRuntime, createOfficialPiLogoutRuntime, createOfficialPiRegistryRuntime, listOfficialPiAuthProviders, logoutOfficialPiProvider, officialPiAuthStatus, runOfficialPiLogin, verifyOfficialPiProviderTurn, } from "../runtime/pi-official-auth.mjs";
|
|
19
19
|
import { documentCommentSubscriptionCapability, markDocumentCommentSubscriptionVerified, } from "../platform/callback-capability.mjs";
|
|
20
|
+
import { openPlatformHost, parseLarkinTenant, registerAppAccountsHost, } from "../feishu/platform-hosts.mjs";
|
|
20
21
|
// qrcode-terminal does not publish TypeScript declarations.
|
|
21
22
|
// @ts-expect-error bundled CommonJS dependency
|
|
22
23
|
import qrcodePackage from "qrcode-terminal";
|
|
@@ -217,6 +218,45 @@ export async function applyDocumentCommentSubscription(input) {
|
|
|
217
218
|
throw new Error("document comment local verified-state persistence failed; external subscription rollback was verified");
|
|
218
219
|
}
|
|
219
220
|
}
|
|
221
|
+
async function resolveSelectedTenant() {
|
|
222
|
+
const raw = flag("--tenant");
|
|
223
|
+
if (raw !== undefined) {
|
|
224
|
+
const parsed = parseLarkinTenant(raw);
|
|
225
|
+
if (parsed === "feishu" || parsed === "lark")
|
|
226
|
+
return parsed;
|
|
227
|
+
die("--tenant 只支持 feishu 或 lark");
|
|
228
|
+
}
|
|
229
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !testFixture) {
|
|
230
|
+
const questioner = terminalSetupQuestioner();
|
|
231
|
+
try {
|
|
232
|
+
const answer = (await questioner.ask("选择平台(授权二维码必须对应正确品牌;lark-cli --domain 是业务域,品牌是 feishu|lark):\n"
|
|
233
|
+
+ " 1. 中国飞书 Feishu(accounts.feishu.cn / https://open.feishu.cn)\n"
|
|
234
|
+
+ " 2. International Lark(accounts.larksuite.com / https://open.larksuite.com)\n> ")).trim();
|
|
235
|
+
if (answer === "1" || /^feishu$/i.test(answer))
|
|
236
|
+
return "feishu";
|
|
237
|
+
if (answer === "2" || /^lark$/i.test(answer))
|
|
238
|
+
return "lark";
|
|
239
|
+
die("必须选择 1/feishu 或 2/lark;未开始授权");
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
questioner.close?.();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return "feishu";
|
|
246
|
+
}
|
|
247
|
+
function persistTenant(selected, userInfo) {
|
|
248
|
+
const brand = userInfo?.tenant_brand;
|
|
249
|
+
if (brand === undefined || brand === null || brand === "")
|
|
250
|
+
return selected;
|
|
251
|
+
const observed = parseLarkinTenant(brand);
|
|
252
|
+
if (observed !== "feishu" && observed !== "lark") {
|
|
253
|
+
die("授权返回的租户品牌非法;未执行凭证同步、文件写入或 Agent 绑定");
|
|
254
|
+
}
|
|
255
|
+
if (observed !== selected) {
|
|
256
|
+
die(`授权返回的租户 ${observed} 与预先选择的 ${selected} 不一致;未执行凭证同步、文件写入或 Agent 绑定`);
|
|
257
|
+
}
|
|
258
|
+
return selected;
|
|
259
|
+
}
|
|
220
260
|
function botVerificationRetryable(result) {
|
|
221
261
|
const text = `${typeof result.stdout === "string" ? result.stdout : ""}\n${typeof result.stderr === "string" ? result.stderr : ""}`;
|
|
222
262
|
return /\binvalid_client\b|specified app does not exist|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|fetch failed|temporary network|too many requests|\b(?:429|502|503|504)\b/i.test(text);
|
|
@@ -374,7 +414,7 @@ async function fetchBotInfoViaHttp(appId, tenant) {
|
|
|
374
414
|
}
|
|
375
415
|
if (typeof credential.appSecret !== "string" || !credential.appSecret)
|
|
376
416
|
return null;
|
|
377
|
-
const base = tenant === "lark" ? "
|
|
417
|
+
const base = openPlatformHost(tenant === "lark" ? "lark" : "feishu");
|
|
378
418
|
try {
|
|
379
419
|
const tokenResponse = await fetch(`${base}/open-apis/auth/v3/tenant_access_token/internal`, {
|
|
380
420
|
method: "POST",
|
|
@@ -424,7 +464,10 @@ export async function main() {
|
|
|
424
464
|
say(`setup 内部机器人注册阶段
|
|
425
465
|
|
|
426
466
|
用法:
|
|
427
|
-
bun bot-register.mjs --auto
|
|
467
|
+
bun bot-register.mjs --auto [--tenant feishu|lark]
|
|
468
|
+
|
|
469
|
+
--tenant 在授权二维码之前选择品牌。默认 feishu(accounts.feishu.cn);
|
|
470
|
+
Lark 必须用 --tenant lark 或交互选择,授权主机是 accounts.larksuite.com。
|
|
428
471
|
|
|
429
472
|
完成后自动:按返回 appId 判定新旧 → 同步并校验 lark-cli bot 凭证 →
|
|
430
473
|
原子写 bots/<appId>.json(0600)→ 绑定 Agent。`);
|
|
@@ -447,7 +490,8 @@ export async function main() {
|
|
|
447
490
|
}
|
|
448
491
|
if (!autoSelect)
|
|
449
492
|
die("setup 注册阶段必须由交互式选择流程启动");
|
|
450
|
-
|
|
493
|
+
const selectedTenant = await resolveSelectedTenant();
|
|
494
|
+
say(`[setup 1/5] 在${selectedTenant === "lark" ? " International Lark" : "中国飞书 Feishu"}网页选择已有机器人或创建新机器人`);
|
|
451
495
|
if (commentSubscription === "application") {
|
|
452
496
|
say(`! 已显式选择 ${commentSubscription} 维度评论订阅:一旦平台状态验证为已订阅,Bot 可见文档中实际送达的每条支持评论都会进入 Inbox 并唤醒 Agent,不要求 @Bot。`);
|
|
453
497
|
say("! setup 将通过官方 lark-cli 的结构化 API 请求创建该订阅,并以只读 subscription_status 二次核验;不会从意图或事件配置推断订阅已生效。");
|
|
@@ -483,6 +527,8 @@ export async function main() {
|
|
|
483
527
|
let pollingCount = 0;
|
|
484
528
|
const result = await registerApp({
|
|
485
529
|
source: "larkin",
|
|
530
|
+
domain: registerAppAccountsHost(selectedTenant),
|
|
531
|
+
larkDomain: registerAppAccountsHost("lark"),
|
|
486
532
|
addons: {
|
|
487
533
|
scopes: { tenant: TENANT_SCOPES },
|
|
488
534
|
events: { items: { tenant: TENANT_EVENTS } },
|
|
@@ -509,9 +555,9 @@ export async function main() {
|
|
|
509
555
|
die("授权完成但未取得有效凭证;未执行凭证同步、文件写入或 Agent 绑定");
|
|
510
556
|
const id = rawId;
|
|
511
557
|
const secret = rawSecret;
|
|
558
|
+
const tenant = persistTenant(selectedTenant, userInfo);
|
|
512
559
|
const botsDir = ensureSecureBotsDir();
|
|
513
560
|
say(`[setup 3/5] 正在按 App ID ${id} 刷新凭证并配置 Agent`);
|
|
514
|
-
const tenant = userInfo?.tenant_brand === "lark" ? "lark" : "feishu";
|
|
515
561
|
const botFile = path.join(botsDir, `${id}.json`);
|
|
516
562
|
const prior = (() => {
|
|
517
563
|
try {
|
|
@@ -8,6 +8,8 @@ import { registerApp as channelRegisterApp } from "@larksuite/channel";
|
|
|
8
8
|
import { markSetupCapabilitiesRequested } from "../platform/callback-capability.mjs";
|
|
9
9
|
import * as larkinConfig from "../platform/config.mjs";
|
|
10
10
|
import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
|
|
11
|
+
import { loadValidatedBotCredential } from "./run-credential-preflight.mjs";
|
|
12
|
+
import { parseLarkinTenant, registerAppAccountsHost } from "../feishu/platform-hosts.mjs";
|
|
11
13
|
// qrcode-terminal does not publish TypeScript declarations.
|
|
12
14
|
// @ts-expect-error bundled CommonJS dependency
|
|
13
15
|
import qrcodePackage from "qrcode-terminal";
|
|
@@ -41,7 +43,19 @@ if (explicitAppId && explicitAppId !== selected.feishuAppId)
|
|
|
41
43
|
if (!/^cli_[A-Za-z0-9]+$/.test(APP_ID))
|
|
42
44
|
throw new Error(`无效飞书(Lark) App ID:${APP_ID}`);
|
|
43
45
|
const SEND_TO = flag("--send-to", "");
|
|
44
|
-
const
|
|
46
|
+
const explicitTenant = argv.includes("--tenant") ? flag("--tenant", "") : "";
|
|
47
|
+
if (argv.includes("--tenant") && !parseLarkinTenant(explicitTenant)) {
|
|
48
|
+
throw new Error("--tenant 只支持 feishu 或 lark");
|
|
49
|
+
}
|
|
50
|
+
function storedCredentialTenant() {
|
|
51
|
+
try {
|
|
52
|
+
return loadValidatedBotCredential(path.join(config.larkinHome, "bots"), APP_ID).tenant;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return "feishu";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const TENANT = parseLarkinTenant(explicitTenant) ?? storedCredentialTenant();
|
|
45
59
|
const WAIT_MIN = Number(flag("--wait-min", "9"));
|
|
46
60
|
const URL_FILE = flag("--url-file", "");
|
|
47
61
|
const TENANT_SCOPES = [
|
|
@@ -108,9 +122,8 @@ export async function main() {
|
|
|
108
122
|
sendUrlToChat(info.url, minutes);
|
|
109
123
|
},
|
|
110
124
|
onStatusChange: (info) => log(`[grant] 状态: ${info.status}`),
|
|
125
|
+
domain: registerAppAccountsHost(TENANT),
|
|
111
126
|
};
|
|
112
|
-
if (TENANT === "lark")
|
|
113
|
-
options.domain = "lark";
|
|
114
127
|
let result;
|
|
115
128
|
try {
|
|
116
129
|
result = await registerApp(options);
|