@lark-project/openclaw-lark-project 2026.3.167 → 2026.3.169
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/dist/src/core/config-schema.js +6 -1
- package/dist/src/core/config-schema.js.map +2 -2
- package/dist/src/core/device-flow.js +2 -2
- package/dist/src/core/device-flow.js.map +2 -2
- package/dist/src/tools/mcp/project/endpoint.js +15 -8
- package/dist/src/tools/mcp/project/endpoint.js.map +2 -2
- package/package.json +1 -1
- package/skills/feishu-project/SKILL.md +26 -0
|
@@ -13,6 +13,10 @@ const ReplyModeSchema = z.union([
|
|
|
13
13
|
]).optional();
|
|
14
14
|
const ChunkModeEnum = z.enum(["newline", "paragraph", "none"]);
|
|
15
15
|
const DomainSchema = z.union([z.literal("feishu"), z.literal("lark"), z.string().regex(/^https:\/\//)]).optional();
|
|
16
|
+
const ProjectDomainSchema = z.union([z.literal("feishu"), z.literal("meegle"), z.string().regex(/^https:\/\//)]).optional();
|
|
17
|
+
const FeishuProjectConfigSchema = z.object({
|
|
18
|
+
domain: ProjectDomainSchema
|
|
19
|
+
}).optional();
|
|
16
20
|
const AllowFromSchema = z.union([z.string(), z.array(z.string())]).optional().transform((v) => {
|
|
17
21
|
if (v === void 0 || v === null) return void 0;
|
|
18
22
|
return Array.isArray(v) ? v : [v];
|
|
@@ -117,7 +121,8 @@ const FeishuAccountConfigSchema = z.object({
|
|
|
117
121
|
dedup: DedupSchema,
|
|
118
122
|
reactionNotifications: ReactionNotificationModeSchema,
|
|
119
123
|
threadSession: z.boolean().optional(),
|
|
120
|
-
uat: UATConfigSchema
|
|
124
|
+
uat: UATConfigSchema,
|
|
125
|
+
project: FeishuProjectConfigSchema
|
|
121
126
|
});
|
|
122
127
|
const FeishuConfigSchema = FeishuAccountConfigSchema.extend({
|
|
123
128
|
accounts: z.record(z.string(), FeishuAccountConfigSchema).optional()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/core/config-schema.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n *\n * Zod-based configuration schema for the OpenClaw Lark/Feishu channel plugin.\n *\n * Provides runtime validation, sensible defaults, and cross-field refinements\n * so that every consuming module can rely on well-typed configuration objects.\n */\n\nimport { z, toJSONSchema } from 'zod';\n\nexport { z };\n\n// ---------------------------------------------------------------------------\n// Shared micro-schemas\n// ---------------------------------------------------------------------------\n\nconst DmPolicyEnum = z.enum(['open', 'pairing', 'allowlist', 'disabled']);\nconst GroupPolicyEnum = z.enum(['open', 'allowlist', 'disabled']);\nconst ConnectionModeEnum = z.enum(['websocket', 'webhook']);\nconst ReplyModeValue = z.enum(['auto', 'static', 'streaming']);\nconst ReplyModeSchema = z\n .union([\n ReplyModeValue,\n z.object({\n default: ReplyModeValue.optional(),\n group: ReplyModeValue.optional(),\n direct: ReplyModeValue.optional(),\n }),\n ])\n .optional();\nconst ChunkModeEnum = z.enum(['newline', 'paragraph', 'none']);\n\nconst DomainSchema = z.union([z.literal('feishu'), z.literal('lark'), z.string().regex(/^https:\\/\\//)]).optional();\n\nconst AllowFromSchema = z\n .union([z.string(), z.array(z.string())])\n .optional()\n .transform((v) => {\n if (v === undefined || v === null) return undefined;\n return Array.isArray(v) ? v : [v];\n });\n\nconst ToolPolicySchema = z\n .object({\n allow: z.array(z.string()).optional(),\n deny: z.array(z.string()).optional(),\n })\n .optional();\n\nconst FeishuToolsFlagSchema = z\n .object({\n doc: z.boolean().optional(),\n wiki: z.boolean().optional(),\n drive: z.boolean().optional(),\n perm: z.boolean().optional(),\n scopes: z.boolean().optional(),\n })\n .optional();\n\nconst FeishuFooterSchema = z\n .object({\n status: z.boolean().optional(),\n elapsed: z.boolean().optional(),\n })\n .optional();\n\nconst BlockStreamingCoalesceSchema = z\n .object({\n minChars: z.number().optional(),\n maxChars: z.number().optional(),\n idleMs: z.number().optional(),\n })\n .optional();\n\nconst MarkdownConfigSchema = z\n .object({\n tables: z.enum(['off', 'bullets', 'code']).optional(),\n })\n .optional();\n\nconst HeartbeatSchema = z\n .object({\n every: z.string().optional(),\n activeHours: z\n .object({\n start: z.string().optional(),\n end: z.string().optional(),\n timezone: z.string().optional(),\n })\n .optional(),\n target: z.string().optional(),\n to: z.string().optional(),\n prompt: z.string().optional(),\n accountId: z.string().optional(),\n })\n .optional();\n\nconst CapabilitiesSchema = z\n .object({\n image: z.boolean().optional(),\n audio: z.boolean().optional(),\n video: z.boolean().optional(),\n })\n .optional();\n\nconst DedupSchema = z\n .object({\n ttlMs: z.number().optional(), // default 43200000 (12h)\n maxEntries: z.number().optional(), // default 5000\n })\n .optional();\n\nconst ReactionNotificationModeSchema = z.enum(['off', 'own', 'all']).optional();\n\nexport const UATConfigSchema = z\n .object({\n enabled: z.boolean().optional(),\n allowedScopes: z.array(z.string()).optional(),\n blockedScopes: z.array(z.string()).optional(),\n })\n .optional();\n\nconst DmConfigSchema = z\n .object({\n historyLimit: z.number().optional(),\n })\n .optional();\n\n// ---------------------------------------------------------------------------\n// Group schema\n// ---------------------------------------------------------------------------\n\nexport const FeishuGroupSchema = z.object({\n groupPolicy: GroupPolicyEnum.optional(),\n requireMention: z.boolean().optional(),\n tools: ToolPolicySchema,\n skills: z.array(z.string()).optional(),\n enabled: z.boolean().optional(),\n allowFrom: AllowFromSchema,\n systemPrompt: z.string().optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Account config schema (same shape as top-level minus `accounts`)\n// ---------------------------------------------------------------------------\n\nexport const FeishuAccountConfigSchema = z.object({\n appId: z.string().optional(),\n appSecret: z.string().optional(),\n encryptKey: z.string().optional(),\n verificationToken: z.string().optional(),\n name: z.string().optional(),\n enabled: z.boolean().optional(),\n domain: DomainSchema,\n connectionMode: ConnectionModeEnum.optional(),\n webhookPath: z.string().optional(),\n webhookPort: z.number().optional(),\n dmPolicy: DmPolicyEnum.optional(),\n allowFrom: AllowFromSchema,\n groupPolicy: GroupPolicyEnum.optional(),\n groupAllowFrom: AllowFromSchema,\n requireMention: z.boolean().optional(),\n groups: z.record(z.string(), FeishuGroupSchema).optional(),\n historyLimit: z.number().optional(),\n dmHistoryLimit: z.number().optional(),\n dms: DmConfigSchema,\n textChunkLimit: z.number().optional(),\n chunkMode: ChunkModeEnum.optional(),\n blockStreamingCoalesce: BlockStreamingCoalesceSchema,\n mediaMaxMb: z.number().optional(),\n heartbeat: HeartbeatSchema,\n replyMode: ReplyModeSchema,\n streaming: z.boolean().optional(),\n blockStreaming: z.boolean().optional(),\n tools: FeishuToolsFlagSchema,\n footer: FeishuFooterSchema,\n markdown: MarkdownConfigSchema,\n configWrites: z.boolean().optional(),\n capabilities: CapabilitiesSchema,\n dedup: DedupSchema,\n reactionNotifications: ReactionNotificationModeSchema,\n threadSession: z.boolean().optional(),\n uat: UATConfigSchema,\n});\n\n// ---------------------------------------------------------------------------\n// Top-level Feishu config schema\n// ---------------------------------------------------------------------------\n\nexport const FeishuConfigSchema = FeishuAccountConfigSchema.extend({\n accounts: z.record(z.string(), FeishuAccountConfigSchema).optional(),\n}).superRefine((data, ctx) => {\n // When dmPolicy is \"open\", allowFrom must contain the wildcard \"*\".\n if (data.dmPolicy === 'open') {\n const list = data.allowFrom;\n const hasWildcard = Array.isArray(list) && list.includes('*');\n\n if (!hasWildcard) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['allowFrom'],\n message: 'When dmPolicy is \"open\", allowFrom must include \"*\" to permit all senders.',\n });\n }\n }\n});\n\n// ---------------------------------------------------------------------------\n// Auto-generated JSON Schema (single source of truth)\n// ---------------------------------------------------------------------------\n\n/**\n * JSON Schema derived from FeishuConfigSchema.\n *\n * - `io: \"input\"` exposes the input type for `.transform()` schemas (e.g. AllowFromSchema).\n * - `unrepresentable: \"any\"` degrades `.superRefine()` constraints to `{}`.\n * - `target: \"draft-07\"` matches the plugin system's expected JSON Schema version.\n */\nexport const FEISHU_CONFIG_JSON_SCHEMA: Record<string, unknown> = toJSONSchema(FeishuConfigSchema, {\n target: 'draft-07',\n io: 'input',\n unrepresentable: 'any',\n});\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n *\n * Zod-based configuration schema for the OpenClaw Lark/Feishu channel plugin.\n *\n * Provides runtime validation, sensible defaults, and cross-field refinements\n * so that every consuming module can rely on well-typed configuration objects.\n */\n\nimport { z, toJSONSchema } from 'zod';\n\nexport { z };\n\n// ---------------------------------------------------------------------------\n// Shared micro-schemas\n// ---------------------------------------------------------------------------\n\nconst DmPolicyEnum = z.enum(['open', 'pairing', 'allowlist', 'disabled']);\nconst GroupPolicyEnum = z.enum(['open', 'allowlist', 'disabled']);\nconst ConnectionModeEnum = z.enum(['websocket', 'webhook']);\nconst ReplyModeValue = z.enum(['auto', 'static', 'streaming']);\nconst ReplyModeSchema = z\n .union([\n ReplyModeValue,\n z.object({\n default: ReplyModeValue.optional(),\n group: ReplyModeValue.optional(),\n direct: ReplyModeValue.optional(),\n }),\n ])\n .optional();\nconst ChunkModeEnum = z.enum(['newline', 'paragraph', 'none']);\n\nconst DomainSchema = z.union([z.literal('feishu'), z.literal('lark'), z.string().regex(/^https:\\/\\//)]).optional();\n\nconst ProjectDomainSchema = z\n .union([z.literal('feishu'), z.literal('meegle'), z.string().regex(/^https:\\/\\//)])\n .optional();\n\nconst FeishuProjectConfigSchema = z\n .object({\n domain: ProjectDomainSchema,\n })\n .optional();\n\nconst AllowFromSchema = z\n .union([z.string(), z.array(z.string())])\n .optional()\n .transform((v) => {\n if (v === undefined || v === null) return undefined;\n return Array.isArray(v) ? v : [v];\n });\n\nconst ToolPolicySchema = z\n .object({\n allow: z.array(z.string()).optional(),\n deny: z.array(z.string()).optional(),\n })\n .optional();\n\nconst FeishuToolsFlagSchema = z\n .object({\n doc: z.boolean().optional(),\n wiki: z.boolean().optional(),\n drive: z.boolean().optional(),\n perm: z.boolean().optional(),\n scopes: z.boolean().optional(),\n })\n .optional();\n\nconst FeishuFooterSchema = z\n .object({\n status: z.boolean().optional(),\n elapsed: z.boolean().optional(),\n })\n .optional();\n\nconst BlockStreamingCoalesceSchema = z\n .object({\n minChars: z.number().optional(),\n maxChars: z.number().optional(),\n idleMs: z.number().optional(),\n })\n .optional();\n\nconst MarkdownConfigSchema = z\n .object({\n tables: z.enum(['off', 'bullets', 'code']).optional(),\n })\n .optional();\n\nconst HeartbeatSchema = z\n .object({\n every: z.string().optional(),\n activeHours: z\n .object({\n start: z.string().optional(),\n end: z.string().optional(),\n timezone: z.string().optional(),\n })\n .optional(),\n target: z.string().optional(),\n to: z.string().optional(),\n prompt: z.string().optional(),\n accountId: z.string().optional(),\n })\n .optional();\n\nconst CapabilitiesSchema = z\n .object({\n image: z.boolean().optional(),\n audio: z.boolean().optional(),\n video: z.boolean().optional(),\n })\n .optional();\n\nconst DedupSchema = z\n .object({\n ttlMs: z.number().optional(), // default 43200000 (12h)\n maxEntries: z.number().optional(), // default 5000\n })\n .optional();\n\nconst ReactionNotificationModeSchema = z.enum(['off', 'own', 'all']).optional();\n\nexport const UATConfigSchema = z\n .object({\n enabled: z.boolean().optional(),\n allowedScopes: z.array(z.string()).optional(),\n blockedScopes: z.array(z.string()).optional(),\n })\n .optional();\n\nconst DmConfigSchema = z\n .object({\n historyLimit: z.number().optional(),\n })\n .optional();\n\n// ---------------------------------------------------------------------------\n// Group schema\n// ---------------------------------------------------------------------------\n\nexport const FeishuGroupSchema = z.object({\n groupPolicy: GroupPolicyEnum.optional(),\n requireMention: z.boolean().optional(),\n tools: ToolPolicySchema,\n skills: z.array(z.string()).optional(),\n enabled: z.boolean().optional(),\n allowFrom: AllowFromSchema,\n systemPrompt: z.string().optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Account config schema (same shape as top-level minus `accounts`)\n// ---------------------------------------------------------------------------\n\nexport const FeishuAccountConfigSchema = z.object({\n appId: z.string().optional(),\n appSecret: z.string().optional(),\n encryptKey: z.string().optional(),\n verificationToken: z.string().optional(),\n name: z.string().optional(),\n enabled: z.boolean().optional(),\n domain: DomainSchema,\n connectionMode: ConnectionModeEnum.optional(),\n webhookPath: z.string().optional(),\n webhookPort: z.number().optional(),\n dmPolicy: DmPolicyEnum.optional(),\n allowFrom: AllowFromSchema,\n groupPolicy: GroupPolicyEnum.optional(),\n groupAllowFrom: AllowFromSchema,\n requireMention: z.boolean().optional(),\n groups: z.record(z.string(), FeishuGroupSchema).optional(),\n historyLimit: z.number().optional(),\n dmHistoryLimit: z.number().optional(),\n dms: DmConfigSchema,\n textChunkLimit: z.number().optional(),\n chunkMode: ChunkModeEnum.optional(),\n blockStreamingCoalesce: BlockStreamingCoalesceSchema,\n mediaMaxMb: z.number().optional(),\n heartbeat: HeartbeatSchema,\n replyMode: ReplyModeSchema,\n streaming: z.boolean().optional(),\n blockStreaming: z.boolean().optional(),\n tools: FeishuToolsFlagSchema,\n footer: FeishuFooterSchema,\n markdown: MarkdownConfigSchema,\n configWrites: z.boolean().optional(),\n capabilities: CapabilitiesSchema,\n dedup: DedupSchema,\n reactionNotifications: ReactionNotificationModeSchema,\n threadSession: z.boolean().optional(),\n uat: UATConfigSchema,\n project: FeishuProjectConfigSchema,\n});\n\n// ---------------------------------------------------------------------------\n// Top-level Feishu config schema\n// ---------------------------------------------------------------------------\n\nexport const FeishuConfigSchema = FeishuAccountConfigSchema.extend({\n accounts: z.record(z.string(), FeishuAccountConfigSchema).optional(),\n}).superRefine((data, ctx) => {\n // When dmPolicy is \"open\", allowFrom must contain the wildcard \"*\".\n if (data.dmPolicy === 'open') {\n const list = data.allowFrom;\n const hasWildcard = Array.isArray(list) && list.includes('*');\n\n if (!hasWildcard) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n path: ['allowFrom'],\n message: 'When dmPolicy is \"open\", allowFrom must include \"*\" to permit all senders.',\n });\n }\n }\n});\n\n// ---------------------------------------------------------------------------\n// Auto-generated JSON Schema (single source of truth)\n// ---------------------------------------------------------------------------\n\n/**\n * JSON Schema derived from FeishuConfigSchema.\n *\n * - `io: \"input\"` exposes the input type for `.transform()` schemas (e.g. AllowFromSchema).\n * - `unrepresentable: \"any\"` degrades `.superRefine()` constraints to `{}`.\n * - `target: \"draft-07\"` matches the plugin system's expected JSON Schema version.\n */\nexport const FEISHU_CONFIG_JSON_SCHEMA: Record<string, unknown> = toJSONSchema(FeishuConfigSchema, {\n target: 'draft-07',\n io: 'input',\n unrepresentable: 'any',\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -30,7 +30,7 @@ function resolveOAuthEndpoints(brand) {
|
|
|
30
30
|
}
|
|
31
31
|
async function requestDeviceAuthorization(params) {
|
|
32
32
|
const { appId, appSecret, brand } = params;
|
|
33
|
-
const endpoints =
|
|
33
|
+
const endpoints = resolveOAuthEndpoints(brand);
|
|
34
34
|
let scope = params.scope ?? "";
|
|
35
35
|
if (!scope.includes("offline_access")) {
|
|
36
36
|
scope = scope ? `${scope} offline_access` : "offline_access";
|
|
@@ -92,7 +92,7 @@ async function pollDeviceToken(params) {
|
|
|
92
92
|
const MAX_POLL_ATTEMPTS = 200;
|
|
93
93
|
const { appId, appSecret, brand, deviceCode, expiresIn, signal } = params;
|
|
94
94
|
let interval = params.interval;
|
|
95
|
-
const endpoints =
|
|
95
|
+
const endpoints = resolveOAuthEndpoints(brand);
|
|
96
96
|
const deadline = Date.now() + expiresIn * 1e3;
|
|
97
97
|
let attempts = 0;
|
|
98
98
|
while (Date.now() < deadline && attempts < MAX_POLL_ATTEMPTS) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/core/device-flow.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n *\n * OAuth 2.0 Device Authorization Grant (RFC 8628) for Lark/Feishu.\n *\n * Two-step flow:\n * 1. `requestDeviceAuthorization` \u2013 obtains device_code + user_code.\n * 2. `pollDeviceToken` \u2013 polls the token endpoint until the user authorises,\n * rejects, or the code expires.\n *\n * All HTTP calls use the built-in `fetch` (Node 18+). The Lark SDK is not\n * used here because these OAuth endpoints are outside the SDK's scope.\n */\n\nimport type { LarkBrand } from './types';\nimport { larkLogger } from './lark-logger';\n\nconst log = larkLogger('core/device-flow');\nimport { feishuFetch } from './feishu-fetch';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DeviceAuthResponse {\n deviceCode: string;\n userCode: string;\n verificationUri: string;\n verificationUriComplete: string;\n expiresIn: number; // seconds\n interval: number; // recommended polling interval (seconds)\n}\n\nexport interface DeviceFlowTokenData {\n accessToken: string;\n refreshToken: string;\n expiresIn: number; // seconds\n refreshExpiresIn: number; // seconds\n scope: string;\n}\n\nexport type DeviceFlowResult =\n | { ok: true; token: DeviceFlowTokenData }\n | { ok: false; error: DeviceFlowError; message: string };\n\nexport type DeviceFlowError = 'authorization_pending' | 'slow_down' | 'access_denied' | 'expired_token';\n\n// ---------------------------------------------------------------------------\n// Endpoint resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the two OAuth endpoint URLs based on the configured brand.\n */\nexport function resolveOAuthEndpoints(brand: LarkBrand): {\n deviceAuthorization: string;\n token: string;\n} {\n if (!brand || brand === 'feishu') {\n return {\n deviceAuthorization: 'https://accounts.feishu.cn/oauth/v1/device_authorization',\n token: 'https://open.feishu.cn/open-apis/authen/v2/oauth/token',\n };\n }\n if (brand === 'lark') {\n return {\n deviceAuthorization: 'https://accounts.larksuite.com/oauth/v1/device_authorization',\n token: 'https://open.larksuite.com/open-apis/authen/v2/oauth/token',\n };\n }\n // Custom domain \u2013 derive paths by convention.\n // Smart derivation: open.X \u2192 accounts.X for the device authorization endpoint.\n const base = brand.replace(/\\/+$/, '');\n let accountsBase = base;\n try {\n const parsed = new URL(base);\n if (parsed.hostname.startsWith('open.')) {\n accountsBase = `${parsed.protocol}//${parsed.hostname.replace(/^open\\./, 'accounts.')}`;\n }\n } catch {\n /* fallback to base */\n }\n\n return {\n deviceAuthorization: `${accountsBase}/oauth/v1/device_authorization`,\n token: `${base}/open-apis/authen/v2/oauth/token`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Step 1 \u2013 Device Authorization Request\n// ---------------------------------------------------------------------------\n\n/**\n * Request a device authorisation code from the Feishu OAuth server.\n *\n * Uses Confidential Client authentication (HTTP Basic with appId:appSecret).\n * The `offline_access` scope is automatically appended so that the token\n * response includes a refresh_token.\n */\nexport async function requestDeviceAuthorization(params: {\n appId: string;\n appSecret: string;\n brand: LarkBrand;\n scope?: string;\n endpoints?: { deviceAuthorization: string; token: string };\n}): Promise<DeviceAuthResponse> {\n const { appId, appSecret, brand } = params;\n const endpoints = params.endpoints || resolveOAuthEndpoints(brand);\n\n // Ensure offline_access is always requested.\n let scope = params.scope ?? '';\n if (!scope.includes('offline_access')) {\n scope = scope ? `${scope} offline_access` : 'offline_access';\n }\n\n const basicAuth = Buffer.from(`${appId}:${appSecret}`).toString('base64');\n\n const body = new URLSearchParams();\n body.set('client_id', appId);\n body.set('scope', scope);\n\n log.info(\n `requesting device authorization (scope=\"${scope}\") url=${endpoints.deviceAuthorization} token_url=${endpoints.token}`,\n );\n\n const resp = await feishuFetch(endpoints.deviceAuthorization, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Authorization: `Basic ${basicAuth}`,\n },\n body: body.toString(),\n });\n\n const text = await resp.text();\n log.info(`response status=${resp.status} body=${text.slice(0, 500)}`);\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(text) as Record<string, unknown>;\n } catch {\n throw new Error(`Device authorization failed: HTTP ${resp.status} \u2013 ${text.slice(0, 200)}`);\n }\n\n if (!resp.ok || data.error) {\n const msg = (data.error_description as string) ?? (data.error as string) ?? 'Unknown error';\n throw new Error(`Device authorization failed: ${msg}`);\n }\n\n const expiresIn = (data.expires_in as number) ?? 240;\n const interval = (data.interval as number) ?? 5;\n log.info(`device_code obtained, expires_in=${expiresIn}s (${Math.round(expiresIn / 60)}min), interval=${interval}s`);\n\n return {\n deviceCode: data.device_code as string,\n userCode: data.user_code as string,\n verificationUri: data.verification_uri as string,\n verificationUriComplete: (data.verification_uri_complete as string) ?? (data.verification_uri as string),\n expiresIn,\n interval,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Step 2 \u2013 Poll Token Endpoint\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(new DOMException('Aborted', 'AbortError'));\n },\n { once: true },\n );\n });\n}\n\n/**\n * Poll the token endpoint until the user authorises, rejects, or the code\n * expires.\n *\n * Handles `authorization_pending` (keep polling), `slow_down` (back off by\n * +5 s), `access_denied` and `expired_token` (terminal errors).\n *\n * Pass an `AbortSignal` to cancel polling from the outside.\n */\nexport async function pollDeviceToken(params: {\n appId: string;\n appSecret: string;\n brand: LarkBrand;\n deviceCode: string;\n interval: number;\n expiresIn: number;\n signal?: AbortSignal;\n endpoints?: { deviceAuthorization: string; token: string };\n}): Promise<DeviceFlowResult> {\n const MAX_POLL_INTERVAL = 60; // slow_down \u6700\u5927\u95F4\u9694 60 \u79D2\n const MAX_POLL_ATTEMPTS = 200; // \u5B89\u5168\u4E0A\u9650\uFF08\u8FDC\u8D85\u8BBE\u5907\u7801\u6709\u6548\u671F\uFF09\n\n const { appId, appSecret, brand, deviceCode, expiresIn, signal } = params;\n let interval = params.interval;\n const endpoints = params.endpoints || resolveOAuthEndpoints(brand);\n const deadline = Date.now() + expiresIn * 1000;\n let attempts = 0;\n\n while (Date.now() < deadline && attempts < MAX_POLL_ATTEMPTS) {\n attempts++;\n if (signal?.aborted) {\n return { ok: false, error: 'expired_token', message: 'Polling was cancelled' };\n }\n\n await sleep(interval * 1000, signal);\n\n let data: Record<string, unknown>;\n try {\n const resp = await feishuFetch(endpoints.token, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:device_code',\n device_code: deviceCode,\n client_id: appId,\n client_secret: appSecret,\n }).toString(),\n });\n data = (await resp.json()) as Record<string, unknown>;\n } catch (err) {\n log.warn(`poll network error: ${err}`);\n interval = Math.min(interval + 1, MAX_POLL_INTERVAL);\n continue;\n }\n\n const error = data.error as string | undefined;\n\n if (!error && data.access_token) {\n log.info('token obtained successfully');\n const refreshToken = (data.refresh_token as string) ?? '';\n const expiresIn = (data.expires_in as number) ?? 7200;\n let refreshExpiresIn = (data.refresh_token_expires_in as number) ?? 604800;\n if (!refreshToken) {\n log.warn('no refresh_token in response, token will not be refreshable');\n refreshExpiresIn = expiresIn;\n }\n return {\n ok: true,\n token: {\n accessToken: data.access_token as string,\n refreshToken,\n expiresIn,\n refreshExpiresIn,\n scope: (data.scope as string) ?? '',\n },\n };\n }\n\n if (error === 'authorization_pending') {\n log.debug('authorization_pending, retrying...');\n continue;\n }\n\n if (error === 'slow_down') {\n interval = Math.min(interval + 5, MAX_POLL_INTERVAL);\n log.info(`slow_down, interval increased to ${interval}s`);\n continue;\n }\n\n if (error === 'access_denied') {\n log.info('user denied authorization');\n return { ok: false, error: 'access_denied', message: '\u7528\u6237\u62D2\u7EDD\u4E86\u6388\u6743' };\n }\n\n if (error === 'expired_token' || error === 'invalid_grant') {\n log.info(`device code expired/invalid (error=${error})`);\n return { ok: false, error: 'expired_token', message: '\u6388\u6743\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77' };\n }\n\n // Unknown error \u2013 treat as terminal.\n const desc = (data.error_description as string) ?? error ?? 'Unknown error';\n log.warn(`unexpected error: error=${error}, desc=${desc}`);\n return { ok: false, error: 'expired_token', message: desc };\n }\n\n if (attempts >= MAX_POLL_ATTEMPTS) {\n log.warn(`max poll attempts (${MAX_POLL_ATTEMPTS}) reached`);\n }\n return { ok: false, error: 'expired_token', message: '\u6388\u6743\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77' };\n}\n"],
|
|
5
|
-
"mappings": "AAgBA,SAAS,kBAAkB;AAE3B,MAAM,MAAM,WAAW,kBAAkB;AACzC,SAAS,mBAAmB;AAoCrB,SAAS,sBAAsB,OAGpC;AACA,MAAI,CAAC,SAAS,UAAU,UAAU;AAChC,WAAO;AAAA,MACL,qBAAqB;AAAA,MACrB,OAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,UAAU,QAAQ;AACpB,WAAO;AAAA,MACL,qBAAqB;AAAA,MACrB,OAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,OAAO,MAAM,QAAQ,QAAQ,EAAE;AACrC,MAAI,eAAe;AACnB,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,QAAI,OAAO,SAAS,WAAW,OAAO,GAAG;AACvC,qBAAe,GAAG,OAAO,QAAQ,KAAK,OAAO,SAAS,QAAQ,WAAW,WAAW,CAAC;AAAA,IACvF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AAAA,IACL,qBAAqB,GAAG,YAAY;AAAA,IACpC,OAAO,GAAG,IAAI;AAAA,EAChB;AACF;AAaA,eAAsB,2BAA2B,
|
|
4
|
+
"sourcesContent": ["/**\n * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n *\n * OAuth 2.0 Device Authorization Grant (RFC 8628) for Lark/Feishu.\n *\n * Two-step flow:\n * 1. `requestDeviceAuthorization` \u2013 obtains device_code + user_code.\n * 2. `pollDeviceToken` \u2013 polls the token endpoint until the user authorises,\n * rejects, or the code expires.\n *\n * All HTTP calls use the built-in `fetch` (Node 18+). The Lark SDK is not\n * used here because these OAuth endpoints are outside the SDK's scope.\n */\n\nimport type { LarkBrand } from './types';\nimport { larkLogger } from './lark-logger';\n\nconst log = larkLogger('core/device-flow');\nimport { feishuFetch } from './feishu-fetch';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DeviceAuthResponse {\n deviceCode: string;\n userCode: string;\n verificationUri: string;\n verificationUriComplete: string;\n expiresIn: number; // seconds\n interval: number; // recommended polling interval (seconds)\n}\n\nexport interface DeviceFlowTokenData {\n accessToken: string;\n refreshToken: string;\n expiresIn: number; // seconds\n refreshExpiresIn: number; // seconds\n scope: string;\n}\n\nexport type DeviceFlowResult =\n | { ok: true; token: DeviceFlowTokenData }\n | { ok: false; error: DeviceFlowError; message: string };\n\nexport type DeviceFlowError = 'authorization_pending' | 'slow_down' | 'access_denied' | 'expired_token';\n\n// ---------------------------------------------------------------------------\n// Endpoint resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the two OAuth endpoint URLs based on the configured brand.\n */\nexport function resolveOAuthEndpoints(brand: LarkBrand): {\n deviceAuthorization: string;\n token: string;\n} {\n if (!brand || brand === 'feishu') {\n return {\n deviceAuthorization: 'https://accounts.feishu.cn/oauth/v1/device_authorization',\n token: 'https://open.feishu.cn/open-apis/authen/v2/oauth/token',\n };\n }\n if (brand === 'lark') {\n return {\n deviceAuthorization: 'https://accounts.larksuite.com/oauth/v1/device_authorization',\n token: 'https://open.larksuite.com/open-apis/authen/v2/oauth/token',\n };\n }\n // Custom domain \u2013 derive paths by convention.\n // Smart derivation: open.X \u2192 accounts.X for the device authorization endpoint.\n const base = brand.replace(/\\/+$/, '');\n let accountsBase = base;\n try {\n const parsed = new URL(base);\n if (parsed.hostname.startsWith('open.')) {\n accountsBase = `${parsed.protocol}//${parsed.hostname.replace(/^open\\./, 'accounts.')}`;\n }\n } catch {\n /* fallback to base */\n }\n\n return {\n deviceAuthorization: `${accountsBase}/oauth/v1/device_authorization`,\n token: `${base}/open-apis/authen/v2/oauth/token`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Step 1 \u2013 Device Authorization Request\n// ---------------------------------------------------------------------------\n\n/**\n * Request a device authorisation code from the Feishu OAuth server.\n *\n * Uses Confidential Client authentication (HTTP Basic with appId:appSecret).\n * The `offline_access` scope is automatically appended so that the token\n * response includes a refresh_token.\n */\nexport async function requestDeviceAuthorization(params: {\n appId: string;\n appSecret: string;\n brand: LarkBrand;\n scope?: string;\n}): Promise<DeviceAuthResponse> {\n const { appId, appSecret, brand } = params;\n const endpoints = resolveOAuthEndpoints(brand);\n\n // Ensure offline_access is always requested.\n let scope = params.scope ?? '';\n if (!scope.includes('offline_access')) {\n scope = scope ? `${scope} offline_access` : 'offline_access';\n }\n\n const basicAuth = Buffer.from(`${appId}:${appSecret}`).toString('base64');\n\n const body = new URLSearchParams();\n body.set('client_id', appId);\n body.set('scope', scope);\n\n log.info(\n `requesting device authorization (scope=\"${scope}\") url=${endpoints.deviceAuthorization} token_url=${endpoints.token}`,\n );\n\n const resp = await feishuFetch(endpoints.deviceAuthorization, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Authorization: `Basic ${basicAuth}`,\n },\n body: body.toString(),\n });\n\n const text = await resp.text();\n log.info(`response status=${resp.status} body=${text.slice(0, 500)}`);\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(text) as Record<string, unknown>;\n } catch {\n throw new Error(`Device authorization failed: HTTP ${resp.status} \u2013 ${text.slice(0, 200)}`);\n }\n\n if (!resp.ok || data.error) {\n const msg = (data.error_description as string) ?? (data.error as string) ?? 'Unknown error';\n throw new Error(`Device authorization failed: ${msg}`);\n }\n\n const expiresIn = (data.expires_in as number) ?? 240;\n const interval = (data.interval as number) ?? 5;\n log.info(`device_code obtained, expires_in=${expiresIn}s (${Math.round(expiresIn / 60)}min), interval=${interval}s`);\n\n return {\n deviceCode: data.device_code as string,\n userCode: data.user_code as string,\n verificationUri: data.verification_uri as string,\n verificationUriComplete: (data.verification_uri_complete as string) ?? (data.verification_uri as string),\n expiresIn,\n interval,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Step 2 \u2013 Poll Token Endpoint\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(new DOMException('Aborted', 'AbortError'));\n },\n { once: true },\n );\n });\n}\n\n/**\n * Poll the token endpoint until the user authorises, rejects, or the code\n * expires.\n *\n * Handles `authorization_pending` (keep polling), `slow_down` (back off by\n * +5 s), `access_denied` and `expired_token` (terminal errors).\n *\n * Pass an `AbortSignal` to cancel polling from the outside.\n */\nexport async function pollDeviceToken(params: {\n appId: string;\n appSecret: string;\n brand: LarkBrand;\n deviceCode: string;\n interval: number;\n expiresIn: number;\n signal?: AbortSignal;\n}): Promise<DeviceFlowResult> {\n const MAX_POLL_INTERVAL = 60; // slow_down \u6700\u5927\u95F4\u9694 60 \u79D2\n const MAX_POLL_ATTEMPTS = 200; // \u5B89\u5168\u4E0A\u9650\uFF08\u8FDC\u8D85\u8BBE\u5907\u7801\u6709\u6548\u671F\uFF09\n\n const { appId, appSecret, brand, deviceCode, expiresIn, signal } = params;\n let interval = params.interval;\n const endpoints = resolveOAuthEndpoints(brand);\n const deadline = Date.now() + expiresIn * 1000;\n let attempts = 0;\n\n while (Date.now() < deadline && attempts < MAX_POLL_ATTEMPTS) {\n attempts++;\n if (signal?.aborted) {\n return { ok: false, error: 'expired_token', message: 'Polling was cancelled' };\n }\n\n await sleep(interval * 1000, signal);\n\n let data: Record<string, unknown>;\n try {\n const resp = await feishuFetch(endpoints.token, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:device_code',\n device_code: deviceCode,\n client_id: appId,\n client_secret: appSecret,\n }).toString(),\n });\n data = (await resp.json()) as Record<string, unknown>;\n } catch (err) {\n log.warn(`poll network error: ${err}`);\n interval = Math.min(interval + 1, MAX_POLL_INTERVAL);\n continue;\n }\n\n const error = data.error as string | undefined;\n\n if (!error && data.access_token) {\n log.info('token obtained successfully');\n const refreshToken = (data.refresh_token as string) ?? '';\n const expiresIn = (data.expires_in as number) ?? 7200;\n let refreshExpiresIn = (data.refresh_token_expires_in as number) ?? 604800;\n if (!refreshToken) {\n log.warn('no refresh_token in response, token will not be refreshable');\n refreshExpiresIn = expiresIn;\n }\n return {\n ok: true,\n token: {\n accessToken: data.access_token as string,\n refreshToken,\n expiresIn,\n refreshExpiresIn,\n scope: (data.scope as string) ?? '',\n },\n };\n }\n\n if (error === 'authorization_pending') {\n log.debug('authorization_pending, retrying...');\n continue;\n }\n\n if (error === 'slow_down') {\n interval = Math.min(interval + 5, MAX_POLL_INTERVAL);\n log.info(`slow_down, interval increased to ${interval}s`);\n continue;\n }\n\n if (error === 'access_denied') {\n log.info('user denied authorization');\n return { ok: false, error: 'access_denied', message: '\u7528\u6237\u62D2\u7EDD\u4E86\u6388\u6743' };\n }\n\n if (error === 'expired_token' || error === 'invalid_grant') {\n log.info(`device code expired/invalid (error=${error})`);\n return { ok: false, error: 'expired_token', message: '\u6388\u6743\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77' };\n }\n\n // Unknown error \u2013 treat as terminal.\n const desc = (data.error_description as string) ?? error ?? 'Unknown error';\n log.warn(`unexpected error: error=${error}, desc=${desc}`);\n return { ok: false, error: 'expired_token', message: desc };\n }\n\n if (attempts >= MAX_POLL_ATTEMPTS) {\n log.warn(`max poll attempts (${MAX_POLL_ATTEMPTS}) reached`);\n }\n return { ok: false, error: 'expired_token', message: '\u6388\u6743\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77' };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["expiresIn"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
const
|
|
1
|
+
const PROJECT_DOMAIN_MAP = {
|
|
2
|
+
feishu: "https://project.feishu.cn",
|
|
3
|
+
meegle: "https://meegle.com"
|
|
4
|
+
};
|
|
5
|
+
const MCP_PATH = "/mcp_server/v1";
|
|
6
|
+
function resolveProjectBaseUrl(domain) {
|
|
7
|
+
return PROJECT_DOMAIN_MAP[domain] ?? domain.replace(/\/+$/, "");
|
|
8
|
+
}
|
|
2
9
|
function getProjectMcpEndpoint(cfg) {
|
|
3
|
-
|
|
4
|
-
if (envEndpoint) return envEndpoint;
|
|
10
|
+
let domain = "feishu";
|
|
5
11
|
if (cfg && typeof cfg === "object") {
|
|
6
12
|
const channels = cfg.channels;
|
|
7
13
|
if (channels && typeof channels === "object") {
|
|
@@ -9,17 +15,18 @@ function getProjectMcpEndpoint(cfg) {
|
|
|
9
15
|
if (feishu && typeof feishu === "object") {
|
|
10
16
|
const project = feishu.project;
|
|
11
17
|
if (project && typeof project === "object") {
|
|
12
|
-
const
|
|
13
|
-
if (typeof
|
|
14
|
-
|
|
18
|
+
const d = project.domain;
|
|
19
|
+
if (typeof d === "string" && d.trim()) {
|
|
20
|
+
domain = d.trim();
|
|
15
21
|
}
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
|
-
return
|
|
26
|
+
return `${resolveProjectBaseUrl(domain)}${MCP_PATH}`;
|
|
21
27
|
}
|
|
22
28
|
export {
|
|
23
|
-
getProjectMcpEndpoint
|
|
29
|
+
getProjectMcpEndpoint,
|
|
30
|
+
resolveProjectBaseUrl
|
|
24
31
|
};
|
|
25
32
|
//# sourceMappingURL=endpoint.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/tools/mcp/project/endpoint.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n *\n * \u98DE\u4E66\u9879\u76EE MCP Server \u7AEF\u70B9\u914D\u7F6E\n */\n\nconst
|
|
5
|
-
"mappings": "AAOA,MAAM
|
|
4
|
+
"sourcesContent": ["/**\n * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n *\n * \u98DE\u4E66\u9879\u76EE MCP Server \u7AEF\u70B9\u914D\u7F6E\n */\n\nconst PROJECT_DOMAIN_MAP: Record<string, string> = {\n feishu: 'https://project.feishu.cn',\n meegle: 'https://meegle.com',\n};\n\nconst MCP_PATH = '/mcp_server/v1';\n\n/**\n * \u5C06 domain \u7B80\u5199\u6216\u81EA\u5B9A\u4E49 URL \u89E3\u6790\u4E3A\u98DE\u4E66\u9879\u76EE base URL\u3002\n *\n * - `'feishu'` \u2192 `https://project.feishu.cn`\n * - `'meegle'` \u2192 `https://meegle.com`\n * - `'https://custom.example.com/'` \u2192 `https://custom.example.com`\n */\nexport function resolveProjectBaseUrl(domain: string): string {\n return PROJECT_DOMAIN_MAP[domain] ?? domain.replace(/\\/+$/, '');\n}\n\n/**\n * \u83B7\u53D6\u98DE\u4E66\u9879\u76EE MCP \u7AEF\u70B9\u3002\n *\n * \u4F18\u5148\u7EA7\uFF1A\n * 1. \u914D\u7F6E channels.feishu.project.domain \u2192 \u6620\u5C04\u4E3A MCP endpoint\n * 2. \u9ED8\u8BA4\u503C\uFF08\u7B49\u540C domain: 'feishu'\uFF09\u2192 https://project.feishu.cn/mcp_server/v1\n */\nexport function getProjectMcpEndpoint(cfg?: unknown): string {\n let domain = 'feishu';\n\n if (cfg && typeof cfg === 'object') {\n const channels = (cfg as Record<string, unknown>).channels;\n if (channels && typeof channels === 'object') {\n const feishu = (channels as Record<string, unknown>).feishu;\n if (feishu && typeof feishu === 'object') {\n const project = (feishu as Record<string, unknown>).project;\n if (project && typeof project === 'object') {\n const d = (project as Record<string, unknown>).domain;\n if (typeof d === 'string' && d.trim()) {\n domain = d.trim();\n }\n }\n }\n }\n }\n\n return `${resolveProjectBaseUrl(domain)}${MCP_PATH}`;\n}\n"],
|
|
5
|
+
"mappings": "AAOA,MAAM,qBAA6C;AAAA,EACjD,QAAQ;AAAA,EACR,QAAQ;AACV;AAEA,MAAM,WAAW;AASV,SAAS,sBAAsB,QAAwB;AAC5D,SAAO,mBAAmB,MAAM,KAAK,OAAO,QAAQ,QAAQ,EAAE;AAChE;AASO,SAAS,sBAAsB,KAAuB;AAC3D,MAAI,SAAS;AAEb,MAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,UAAM,WAAY,IAAgC;AAClD,QAAI,YAAY,OAAO,aAAa,UAAU;AAC5C,YAAM,SAAU,SAAqC;AACrD,UAAI,UAAU,OAAO,WAAW,UAAU;AACxC,cAAM,UAAW,OAAmC;AACpD,YAAI,WAAW,OAAO,YAAY,UAAU;AAC1C,gBAAM,IAAK,QAAoC;AAC/C,cAAI,OAAO,MAAM,YAAY,EAAE,KAAK,GAAG;AACrC,qBAAS,EAAE,KAAK;AAAA,UAClB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,GAAG,sBAAsB,MAAM,CAAC,GAAG,QAAQ;AACpD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -15,6 +15,32 @@ description: |
|
|
|
15
15
|
|
|
16
16
|
# 飞书项目(Meego)工具使用指南
|
|
17
17
|
|
|
18
|
+
## 环境确认(强制,不可跳过)
|
|
19
|
+
|
|
20
|
+
> **规则:在调用任何 `feishu_project_*` 工具之前,必须先完成环境确认。未完成环境确认之前,禁止调用授权工具或任何飞书项目工具。**
|
|
21
|
+
|
|
22
|
+
执行以下判断流程:
|
|
23
|
+
|
|
24
|
+
**第一步:检查配置 `channels.feishu.project.domain` 的值。**
|
|
25
|
+
|
|
26
|
+
- 如果值**非空**(如 `"feishu"`、`"meegle"` 或 `"https://..."`)→ 环境已确认,直接进入「授权前置检查」。
|
|
27
|
+
- 如果值**为空或未设置** → 执行第二步。
|
|
28
|
+
|
|
29
|
+
**第二步:必须向用户提问,确认使用哪个环境。** 用以下格式询问:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
你使用的是哪个飞书项目环境?
|
|
33
|
+
1. 飞书项目(国内版,project.feishu.cn)
|
|
34
|
+
2. Meegle(meegle.com)
|
|
35
|
+
3. 其他(请提供完整域名,如 https://custom.example.com)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**第三步:根据用户回答执行对应操作。**
|
|
39
|
+
|
|
40
|
+
- 用户选择 **1(飞书项目)** → 无需修改配置,直接进入「授权前置检查」。
|
|
41
|
+
- 用户选择 **2(Meegle)** → 告知用户需要在配置中添加 `channels.feishu.project.domain: "meegle"` 以持久生效,然后进入「授权前置检查」。
|
|
42
|
+
- 用户选择 **3(其他)** → 告知用户需要在配置中添加 `channels.feishu.project.domain: "https://..."` 以持久生效,然后进入「授权前置检查」。
|
|
43
|
+
|
|
18
44
|
## 授权前置检查
|
|
19
45
|
|
|
20
46
|
飞书项目的 OAuth 授权与飞书 IM/文档的授权**完全独立**,不共享 token。首次使用飞书项目功能时,必须先完成独立授权。
|