@xopcai/xopc 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/dist/extensions/feishu/src/adapters/cli-login.d.ts +8 -0
  2. package/dist/extensions/feishu/src/adapters/cli-login.js +225 -0
  3. package/dist/extensions/feishu/src/adapters/cli-login.js.map +1 -0
  4. package/dist/extensions/feishu/src/adapters/onboard-cli.js +1 -105
  5. package/dist/extensions/feishu/src/adapters/onboard-cli.js.map +1 -1
  6. package/dist/extensions/feishu/src/auth/app-registration.d.ts +47 -0
  7. package/dist/extensions/feishu/src/auth/app-registration.js +122 -0
  8. package/dist/extensions/feishu/src/auth/app-registration.js.map +1 -0
  9. package/dist/extensions/feishu/src/plugin.d.ts +2 -0
  10. package/dist/extensions/feishu/src/plugin.js +2 -0
  11. package/dist/extensions/feishu/src/plugin.js.map +1 -1
  12. package/dist/extensions/telegram/src/inbound-processor.js +1 -1
  13. package/dist/extensions/telegram/src/plugin.d.ts +1 -1
  14. package/dist/extensions/telegram/xopc.extension.json +1 -1
  15. package/dist/gateway/static/root/assets/{agents-ByiiWRbv.js → agents-MbH57-L9.js} +2 -2
  16. package/dist/gateway/static/root/assets/{agents-ByiiWRbv.js.map → agents-MbH57-L9.js.map} +1 -1
  17. package/dist/gateway/static/root/assets/{apps-page-BpR0gguZ.js → apps-page-3i3DvI7i.js} +2 -2
  18. package/dist/gateway/static/root/assets/{apps-page-BpR0gguZ.js.map → apps-page-3i3DvI7i.js.map} +1 -1
  19. package/dist/gateway/static/root/assets/channels-settings-CcuSzoB6.js +9 -0
  20. package/dist/gateway/static/root/assets/channels-settings-CcuSzoB6.js.map +1 -0
  21. package/dist/gateway/static/root/assets/{cron-page-D6wtd-hq.js → cron-page-Be1h9Yub.js} +2 -2
  22. package/dist/gateway/static/root/assets/{cron-page-D6wtd-hq.js.map → cron-page-Be1h9Yub.js.map} +1 -1
  23. package/dist/gateway/static/root/assets/{cron-utils-o-QI_XCC.js → cron-utils-CR97EvZS.js} +2 -2
  24. package/dist/gateway/static/root/assets/{cron-utils-o-QI_XCC.js.map → cron-utils-CR97EvZS.js.map} +1 -1
  25. package/dist/gateway/static/root/assets/{dist-D2Td6E_v.js → dist-r_Gy-XJv.js} +2 -2
  26. package/dist/gateway/static/root/assets/{dist-D2Td6E_v.js.map → dist-r_Gy-XJv.js.map} +1 -1
  27. package/dist/gateway/static/root/assets/{extension-debug-page-BIni4Qq4.js → extension-debug-page-QfYEYruq.js} +2 -2
  28. package/dist/gateway/static/root/assets/{extension-debug-page-BIni4Qq4.js.map → extension-debug-page-QfYEYruq.js.map} +1 -1
  29. package/dist/gateway/static/root/assets/{extension-page-BRLScNkx.js → extension-page-4FW-BmKG.js} +2 -2
  30. package/dist/gateway/static/root/assets/{extension-page-BRLScNkx.js.map → extension-page-4FW-BmKG.js.map} +1 -1
  31. package/dist/gateway/static/root/assets/{extension-settings-page-DjiK9Igx.js → extension-settings-page-E_Wq9LL8.js} +2 -2
  32. package/dist/gateway/static/root/assets/{extension-settings-page-DjiK9Igx.js.map → extension-settings-page-E_Wq9LL8.js.map} +1 -1
  33. package/dist/gateway/static/root/assets/{index-KGmhufWu.js → index-CcQtNJKo.js} +60 -54
  34. package/dist/gateway/static/root/assets/{index-KGmhufWu.js.map → index-CcQtNJKo.js.map} +1 -1
  35. package/dist/gateway/static/root/assets/index-D9Wmfh2f.css +1 -0
  36. package/dist/gateway/static/root/assets/{logs-page-C76F4Y0H.js → logs-page-DFhTU-kG.js} +2 -2
  37. package/dist/gateway/static/root/assets/{logs-page-C76F4Y0H.js.map → logs-page-DFhTU-kG.js.map} +1 -1
  38. package/dist/gateway/static/root/assets/{sessions-page-9jwUqGtS.js → sessions-page-wmnnIj6Z.js} +2 -2
  39. package/dist/gateway/static/root/assets/{sessions-page-9jwUqGtS.js.map → sessions-page-wmnnIj6Z.js.map} +1 -1
  40. package/dist/gateway/static/root/assets/settings-page-BTmUXY4s.js +2 -0
  41. package/dist/gateway/static/root/assets/settings-page-BTmUXY4s.js.map +1 -0
  42. package/dist/gateway/static/root/assets/{skills-page-BY1cLNEz.js → skills-page-D-fRbJG0.js} +2 -2
  43. package/dist/gateway/static/root/assets/{skills-page-BY1cLNEz.js.map → skills-page-D-fRbJG0.js.map} +1 -1
  44. package/dist/gateway/static/root/index.html +2 -2
  45. package/dist/package.js +1 -1
  46. package/dist/src/agent/memory/builtin-memory-store.d.ts +2 -1
  47. package/dist/src/agent/memory/builtin-memory-store.js +7 -6
  48. package/dist/src/agent/memory/builtin-memory-store.js.map +1 -1
  49. package/dist/src/agent/prompt/memory/index.d.ts +4 -2
  50. package/dist/src/agent/prompt/memory/index.js +22 -10
  51. package/dist/src/agent/prompt/memory/index.js.map +1 -1
  52. package/dist/src/agent/service.js +1 -1
  53. package/dist/src/agent/tools/factory.js +9 -2
  54. package/dist/src/agent/tools/factory.js.map +1 -1
  55. package/dist/src/agent/tools/index.d.ts +1 -1
  56. package/dist/src/agent/tools/memory-tool.d.ts +7 -2
  57. package/dist/src/agent/tools/memory-tool.js +11 -5
  58. package/dist/src/agent/tools/memory-tool.js.map +1 -1
  59. package/dist/src/channels/registry.d.ts +1 -1
  60. package/dist/src/channels/registry.js +25 -1
  61. package/dist/src/channels/registry.js.map +1 -1
  62. package/dist/src/chat-commands/builtins/config.js +1 -1
  63. package/dist/src/chat-commands/builtins/session.js +1 -1
  64. package/dist/src/chat-commands/index.js +1 -1
  65. package/dist/src/chat-commands/processor.js +1 -1
  66. package/dist/src/cli/commands/channels.js +20 -2
  67. package/dist/src/cli/commands/channels.js.map +1 -1
  68. package/dist/src/cli/commands/gateway/call.d.ts +2 -0
  69. package/dist/src/cli/commands/gateway/call.js +90 -0
  70. package/dist/src/cli/commands/gateway/call.js.map +1 -0
  71. package/dist/src/cli/commands/gateway/health.d.ts +2 -0
  72. package/dist/src/cli/commands/gateway/health.js +77 -0
  73. package/dist/src/cli/commands/gateway/health.js.map +1 -0
  74. package/dist/src/cli/commands/gateway/index.d.ts +3 -0
  75. package/dist/src/cli/commands/gateway/index.js +4 -1
  76. package/dist/src/cli/commands/gateway/probe.d.ts +2 -0
  77. package/dist/src/cli/commands/gateway/probe.js +102 -0
  78. package/dist/src/cli/commands/gateway/probe.js.map +1 -0
  79. package/dist/src/cli/commands/gateway/status.d.ts +0 -3
  80. package/dist/src/cli/commands/gateway/status.js +107 -24
  81. package/dist/src/cli/commands/gateway/status.js.map +1 -1
  82. package/dist/src/cli/commands/gateway.js +7 -1
  83. package/dist/src/cli/commands/gateway.js.map +1 -1
  84. package/dist/src/cli/commands/update.js +19 -1
  85. package/dist/src/cli/commands/update.js.map +1 -1
  86. package/dist/src/cli/utils/gateway-client.d.ts +28 -0
  87. package/dist/src/cli/utils/gateway-client.js +115 -0
  88. package/dist/src/cli/utils/gateway-client.js.map +1 -0
  89. package/dist/src/config/paths-state.d.ts +4 -0
  90. package/dist/src/config/paths-state.js +9 -1
  91. package/dist/src/config/paths-state.js.map +1 -1
  92. package/dist/src/config/reload.d.ts +2 -0
  93. package/dist/src/config/reload.js +9 -1
  94. package/dist/src/config/reload.js.map +1 -1
  95. package/dist/src/config/rules.js +12 -2
  96. package/dist/src/config/rules.js.map +1 -1
  97. package/dist/src/extensions/api.d.ts +6 -1
  98. package/dist/src/extensions/api.js +52 -1
  99. package/dist/src/extensions/api.js.map +1 -1
  100. package/dist/src/extensions/loader.d.ts +6 -1
  101. package/dist/src/extensions/loader.js +20 -1
  102. package/dist/src/extensions/loader.js.map +1 -1
  103. package/dist/src/extensions/normalize-manifest.js +33 -0
  104. package/dist/src/extensions/normalize-manifest.js.map +1 -1
  105. package/dist/src/extensions/sdk/index.d.ts +1 -1
  106. package/dist/src/extensions/sdk/index.js.map +1 -1
  107. package/dist/src/extensions/types/core.d.ts +35 -1
  108. package/dist/src/extensions/types/manifest.d.ts +14 -0
  109. package/dist/src/gateway/hono/lib/config-payload.d.ts +3 -0
  110. package/dist/src/gateway/hono/lib/config-payload.js +1 -0
  111. package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
  112. package/dist/src/gateway/hono/routes/channels.js +111 -0
  113. package/dist/src/gateway/hono/routes/channels.js.map +1 -1
  114. package/dist/src/gateway/hono/routes/commands-skills.js +13 -2
  115. package/dist/src/gateway/hono/routes/commands-skills.js.map +1 -1
  116. package/dist/src/gateway/hono/routes/config.js +81 -0
  117. package/dist/src/gateway/hono/routes/config.js.map +1 -1
  118. package/dist/src/gateway/hono/routes/public-gateway.js +17 -0
  119. package/dist/src/gateway/hono/routes/public-gateway.js.map +1 -1
  120. package/dist/src/gateway/hono/routes/sessions.js +16 -0
  121. package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
  122. package/dist/src/gateway/hono/routes/status.js +31 -7
  123. package/dist/src/gateway/hono/routes/status.js.map +1 -1
  124. package/dist/src/gateway/hono/routes/update.js +118 -15
  125. package/dist/src/gateway/hono/routes/update.js.map +1 -1
  126. package/dist/src/gateway/index.js +1 -1
  127. package/dist/src/gateway/server.js +3 -0
  128. package/dist/src/gateway/server.js.map +1 -1
  129. package/dist/src/gateway/service.d.ts +23 -0
  130. package/dist/src/gateway/service.js +107 -0
  131. package/dist/src/gateway/service.js.map +1 -1
  132. package/dist/src/infra/update-check.js +54 -21
  133. package/dist/src/infra/update-check.js.map +1 -1
  134. package/dist/src/infra/update-lock.d.ts +13 -0
  135. package/dist/src/infra/update-lock.js +67 -0
  136. package/dist/src/infra/update-lock.js.map +1 -0
  137. package/dist/src/infra/update-runner.d.ts +6 -5
  138. package/dist/src/infra/update-runner.js +93 -13
  139. package/dist/src/infra/update-runner.js.map +1 -1
  140. package/dist/src/infra/update-startup.js +37 -11
  141. package/dist/src/infra/update-startup.js.map +1 -1
  142. package/package.json +1 -1
  143. package/dist/gateway/static/root/assets/channels-settings-h3eQwIPi.js +0 -9
  144. package/dist/gateway/static/root/assets/channels-settings-h3eQwIPi.js.map +0 -1
  145. package/dist/gateway/static/root/assets/index-BQNdJlkw.css +0 -1
  146. package/dist/gateway/static/root/assets/settings-page-DNG3Zijx.js +0 -2
  147. package/dist/gateway/static/root/assets/settings-page-DNG3Zijx.js.map +0 -1
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Feishu CLI Login adapter — implements `ChannelCliLoginAdapter`.
3
+ *
4
+ * Provides `xopc channels login --channel feishu` interactive credential setup.
5
+ * Supports QR scan-to-create and manual credential input.
6
+ */
7
+ import type { ChannelCliLoginAdapter } from '@xopcai/xopc/channels/plugins/types.adapters.js';
8
+ export declare const feishuCliLoginAdapter: ChannelCliLoginAdapter;
@@ -0,0 +1,225 @@
1
+ import { beginAppRegistration, initAppRegistration, pollAppRegistration, printQrCode } from "../auth/app-registration.js";
2
+ import fsSync from "node:fs";
3
+ import { confirm, input, select } from "@inquirer/prompts";
4
+ //#region extensions/feishu/src/adapters/cli-login.ts
5
+ /**
6
+ * Feishu CLI Login adapter — implements `ChannelCliLoginAdapter`.
7
+ *
8
+ * Provides `xopc channels login --channel feishu` interactive credential setup.
9
+ * Supports QR scan-to-create and manual credential input.
10
+ */
11
+ function loadConfigFromPath(configPath) {
12
+ try {
13
+ const raw = fsSync.readFileSync(configPath, "utf8");
14
+ return JSON.parse(raw);
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+ function writeConfigToPath(configPath, config) {
20
+ fsSync.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
21
+ }
22
+ function isFeishuConfigured(config) {
23
+ const feishu = config.channels?.feishu;
24
+ if (!feishu) return false;
25
+ const appId = typeof feishu.appId === "string" ? feishu.appId.trim() : "";
26
+ const appSecret = typeof feishu.appSecret === "string" ? feishu.appSecret.trim() : "";
27
+ return Boolean(appId && appSecret);
28
+ }
29
+ function parseAllowlistRaw(raw) {
30
+ if (!raw.trim()) return [];
31
+ return raw.split(/[,\s\n]+/).map((entry) => entry.trim()).filter(Boolean).map((entry) => {
32
+ const asNumber = parseInt(entry, 10);
33
+ return !isNaN(asNumber) && String(asNumber) === entry ? asNumber : entry;
34
+ });
35
+ }
36
+ async function acquireCredentials(params) {
37
+ const domain = await select({
38
+ message: "Feishu/Lark domain:",
39
+ choices: [{
40
+ value: "feishu",
41
+ name: "feishu (open.feishu.cn)",
42
+ description: "China / Feishu"
43
+ }, {
44
+ value: "lark",
45
+ name: "lark (open.larksuite.com)",
46
+ description: "International / Lark"
47
+ }],
48
+ default: "feishu"
49
+ });
50
+ if (await initAppRegistration(domain) ? await confirm({
51
+ message: "Create an app by scanning a QR code (recommended)?",
52
+ default: true
53
+ }) : false) {
54
+ console.log("\nScan this QR code with Feishu/Lark to create an app:\n");
55
+ const begin = await beginAppRegistration(domain);
56
+ await printQrCode(begin.qrUrl);
57
+ console.log("\nWaiting for confirmation...\n");
58
+ const expireInSec = Math.min(begin.expireInSec, Math.floor(params.timeoutMs / 1e3));
59
+ const outcome = await pollAppRegistration({
60
+ deviceCode: begin.deviceCode,
61
+ intervalSec: begin.intervalSec,
62
+ expireInSec,
63
+ initialDomain: domain
64
+ });
65
+ if (outcome.status === "success") {
66
+ console.log(`✅ App created. Domain: "${outcome.result.domain}".\n`);
67
+ return {
68
+ appId: outcome.result.appId,
69
+ appSecret: outcome.result.appSecret,
70
+ domain: outcome.result.domain,
71
+ ownerOpenId: outcome.result.openId
72
+ };
73
+ }
74
+ const reason = outcome.status === "access_denied" ? "User denied authorization." : outcome.status === "expired" ? "Session expired." : outcome.status === "timeout" ? "Scan timed out." : `Error: ${"message" in outcome ? outcome.message : "unknown"}`;
75
+ console.log(`${reason} Falling back to manual input.\n`);
76
+ }
77
+ console.log("📝 Enter Feishu app credentials (from Feishu Open Platform developer console):\n");
78
+ return {
79
+ appId: (await input({
80
+ message: "App ID (cli_xxx):",
81
+ default: params.existingAppId || void 0,
82
+ validate: (value) => value.trim().length > 0 || "App ID cannot be empty"
83
+ })).trim(),
84
+ appSecret: (await input({
85
+ message: "App Secret:",
86
+ validate: (value) => value.trim().length > 0 || "App Secret cannot be empty"
87
+ })).trim(),
88
+ domain
89
+ };
90
+ }
91
+ async function promptSecurityPolicies(ownerOpenId) {
92
+ const dmPolicy = await select({
93
+ message: "DM (private chat) policy:",
94
+ choices: [
95
+ {
96
+ value: "pairing",
97
+ name: "pairing [recommended]",
98
+ description: "New users must /pair"
99
+ },
100
+ {
101
+ value: "allowlist",
102
+ name: "allowlist",
103
+ description: "Only allowlisted users"
104
+ },
105
+ {
106
+ value: "open",
107
+ name: "open",
108
+ description: "Anyone can DM"
109
+ },
110
+ {
111
+ value: "disabled",
112
+ name: "disabled",
113
+ description: "Disable DMs"
114
+ }
115
+ ],
116
+ default: "pairing"
117
+ });
118
+ let allowFrom = [];
119
+ if (dmPolicy === "allowlist") {
120
+ const defaultAllow = ownerOpenId ?? "";
121
+ allowFrom = parseAllowlistRaw(await input({
122
+ message: "Allowed user open_id / union_id (comma-separated):",
123
+ default: defaultAllow
124
+ }) || defaultAllow);
125
+ }
126
+ const groupPolicy = await select({
127
+ message: "Group chat policy:",
128
+ choices: [
129
+ {
130
+ value: "allowlist",
131
+ name: "allowlist [recommended]",
132
+ description: "Only allowlisted groups"
133
+ },
134
+ {
135
+ value: "open",
136
+ name: "open",
137
+ description: "All groups allowed"
138
+ },
139
+ {
140
+ value: "disabled",
141
+ name: "disabled",
142
+ description: "Disable groups"
143
+ }
144
+ ],
145
+ default: "allowlist"
146
+ });
147
+ let groupAllowFrom = [];
148
+ if (groupPolicy === "allowlist") groupAllowFrom = parseAllowlistRaw(await input({
149
+ message: "Allowed group chat IDs (comma-separated, e.g. oc_xxx):",
150
+ default: ""
151
+ }));
152
+ const requireMention = await confirm({
153
+ message: "Require @mention in groups?",
154
+ default: true
155
+ });
156
+ return {
157
+ dmPolicy,
158
+ groupPolicy,
159
+ allowFrom,
160
+ groupAllowFrom,
161
+ requireMention
162
+ };
163
+ }
164
+ const feishuCliLoginAdapter = { async runLogin(params) {
165
+ const { configPath, verbose, timeoutMs = 48e4, accountId, writeConfig = true } = params;
166
+ if (verbose) console.log(`[feishu-login] configPath=${configPath}, timeoutMs=${timeoutMs}`);
167
+ const config = loadConfigFromPath(configPath);
168
+ const existingFeishu = config.channels?.feishu;
169
+ const existingAppId = typeof existingFeishu?.appId === "string" ? existingFeishu.appId : "";
170
+ const alreadyConfigured = isFeishuConfigured(config);
171
+ console.log(`\n${"=".repeat(50)}`);
172
+ console.log("📱 Feishu / Lark login");
173
+ console.log(`${"=".repeat(50)}\n`);
174
+ if (alreadyConfigured) {
175
+ if (!await confirm({
176
+ message: `Feishu is already configured (App ID: ${existingAppId}). Reconfigure?`,
177
+ default: false
178
+ })) {
179
+ console.log("Keeping existing configuration.\n");
180
+ return {
181
+ ok: true,
182
+ message: "Existing configuration kept.",
183
+ accountId
184
+ };
185
+ }
186
+ }
187
+ const credentials = await acquireCredentials({
188
+ existingAppId,
189
+ timeoutMs
190
+ });
191
+ const policies = await promptSecurityPolicies(credentials.ownerOpenId);
192
+ const nextFeishu = {
193
+ ...existingFeishu ?? {},
194
+ enabled: true,
195
+ appId: credentials.appId,
196
+ appSecret: credentials.appSecret,
197
+ domain: credentials.domain,
198
+ connectionMode: existingFeishu?.connectionMode || "websocket",
199
+ dmPolicy: policies.dmPolicy,
200
+ groupPolicy: policies.groupPolicy,
201
+ allowFrom: policies.allowFrom,
202
+ groupAllowFrom: policies.groupAllowFrom,
203
+ requireMention: policies.requireMention
204
+ };
205
+ const nextConfig = {
206
+ ...config,
207
+ channels: {
208
+ ...config.channels,
209
+ feishu: nextFeishu
210
+ }
211
+ };
212
+ if (writeConfig) {
213
+ writeConfigToPath(configPath, nextConfig);
214
+ console.log(`✅ Feishu configuration saved to ${configPath}\n`);
215
+ } else console.log("✅ Feishu credentials acquired (--credentials-only: config not updated).\n");
216
+ return {
217
+ ok: true,
218
+ message: `Feishu login complete (App ID: ${credentials.appId}).`,
219
+ accountId: accountId ?? void 0
220
+ };
221
+ } };
222
+ //#endregion
223
+ export { feishuCliLoginAdapter };
224
+
225
+ //# sourceMappingURL=cli-login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-login.js","names":["fs"],"sources":["../../../../../extensions/feishu/src/adapters/cli-login.ts"],"sourcesContent":["/**\n * Feishu CLI Login adapter — implements `ChannelCliLoginAdapter`.\n *\n * Provides `xopc channels login --channel feishu` interactive credential setup.\n * Supports QR scan-to-create and manual credential input.\n */\n\nimport fs from 'node:fs';\n\nimport { confirm, input, select } from '@inquirer/prompts';\n\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport type { ChannelCliLoginAdapter } from '@xopcai/xopc/channels/plugins/types.adapters.js';\n\nimport {\n initAppRegistration,\n beginAppRegistration,\n pollAppRegistration,\n printQrCode,\n type FeishuDomain,\n} from '../auth/app-registration.js';\n\ntype DmPolicy = 'pairing' | 'allowlist' | 'open' | 'disabled';\ntype GroupPolicy = 'open' | 'disabled' | 'allowlist';\n\nfunction loadConfigFromPath(configPath: string): Config {\n try {\n const raw = fs.readFileSync(configPath, 'utf8');\n return JSON.parse(raw) as Config;\n } catch {\n return {} as Config;\n }\n}\n\nfunction writeConfigToPath(configPath: string, config: Config): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\\n', 'utf8');\n}\n\nfunction isFeishuConfigured(config: Config): boolean {\n const feishu = config.channels?.feishu as Record<string, unknown> | undefined;\n if (!feishu) return false;\n const appId = typeof feishu.appId === 'string' ? feishu.appId.trim() : '';\n const appSecret = typeof feishu.appSecret === 'string' ? feishu.appSecret.trim() : '';\n return Boolean(appId && appSecret);\n}\n\nfunction parseAllowlistRaw(raw: string): Array<string | number> {\n if (!raw.trim()) return [];\n return raw\n .split(/[,\\s\\n]+/)\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map((entry) => {\n const asNumber = parseInt(entry, 10);\n return !isNaN(asNumber) && String(asNumber) === entry ? asNumber : entry;\n });\n}\n\nasync function acquireCredentials(params: {\n existingAppId: string;\n timeoutMs: number;\n}): Promise<{\n appId: string;\n appSecret: string;\n domain: FeishuDomain;\n ownerOpenId?: string;\n}> {\n const domain = await select<FeishuDomain>({\n message: 'Feishu/Lark domain:',\n choices: [\n { value: 'feishu', name: 'feishu (open.feishu.cn)', description: 'China / Feishu' },\n { value: 'lark', name: 'lark (open.larksuite.com)', description: 'International / Lark' },\n ],\n default: 'feishu',\n });\n\n const canScan = await initAppRegistration(domain);\n const useScan = canScan\n ? await confirm({\n message: 'Create an app by scanning a QR code (recommended)?',\n default: true,\n })\n : false;\n\n if (useScan) {\n console.log('\\nScan this QR code with Feishu/Lark to create an app:\\n');\n const begin = await beginAppRegistration(domain);\n await printQrCode(begin.qrUrl);\n console.log('\\nWaiting for confirmation...\\n');\n\n const expireInSec = Math.min(begin.expireInSec, Math.floor(params.timeoutMs / 1000));\n const outcome = await pollAppRegistration({\n deviceCode: begin.deviceCode,\n intervalSec: begin.intervalSec,\n expireInSec,\n initialDomain: domain,\n });\n\n if (outcome.status === 'success') {\n console.log(`✅ App created. Domain: \"${outcome.result.domain}\".\\n`);\n return {\n appId: outcome.result.appId,\n appSecret: outcome.result.appSecret,\n domain: outcome.result.domain,\n ownerOpenId: outcome.result.openId,\n };\n }\n\n const reason =\n outcome.status === 'access_denied'\n ? 'User denied authorization.'\n : outcome.status === 'expired'\n ? 'Session expired.'\n : outcome.status === 'timeout'\n ? 'Scan timed out.'\n : `Error: ${'message' in outcome ? outcome.message : 'unknown'}`;\n console.log(`${reason} Falling back to manual input.\\n`);\n }\n\n console.log('📝 Enter Feishu app credentials (from Feishu Open Platform developer console):\\n');\n\n const appId = (\n await input({\n message: 'App ID (cli_xxx):',\n default: params.existingAppId || undefined,\n validate: (value) => value.trim().length > 0 || 'App ID cannot be empty',\n })\n ).trim();\n\n const appSecret = (\n await input({\n message: 'App Secret:',\n validate: (value) => value.trim().length > 0 || 'App Secret cannot be empty',\n })\n ).trim();\n\n return { appId, appSecret, domain };\n}\n\nasync function promptSecurityPolicies(ownerOpenId?: string): Promise<{\n dmPolicy: DmPolicy;\n groupPolicy: GroupPolicy;\n allowFrom: Array<string | number>;\n groupAllowFrom: Array<string | number>;\n requireMention: boolean;\n}> {\n const dmPolicy = await select<DmPolicy>({\n message: 'DM (private chat) policy:',\n choices: [\n { value: 'pairing', name: 'pairing [recommended]', description: 'New users must /pair' },\n { value: 'allowlist', name: 'allowlist', description: 'Only allowlisted users' },\n { value: 'open', name: 'open', description: 'Anyone can DM' },\n { value: 'disabled', name: 'disabled', description: 'Disable DMs' },\n ],\n default: 'pairing',\n });\n\n let allowFrom: Array<string | number> = [];\n if (dmPolicy === 'allowlist') {\n const defaultAllow = ownerOpenId ?? '';\n const raw = await input({\n message: 'Allowed user open_id / union_id (comma-separated):',\n default: defaultAllow,\n });\n allowFrom = parseAllowlistRaw(raw || defaultAllow);\n }\n\n const groupPolicy = await select<GroupPolicy>({\n message: 'Group chat policy:',\n choices: [\n { value: 'allowlist', name: 'allowlist [recommended]', description: 'Only allowlisted groups' },\n { value: 'open', name: 'open', description: 'All groups allowed' },\n { value: 'disabled', name: 'disabled', description: 'Disable groups' },\n ],\n default: 'allowlist',\n });\n\n let groupAllowFrom: Array<string | number> = [];\n if (groupPolicy === 'allowlist') {\n const raw = await input({\n message: 'Allowed group chat IDs (comma-separated, e.g. oc_xxx):',\n default: '',\n });\n groupAllowFrom = parseAllowlistRaw(raw);\n }\n\n const requireMention = await confirm({\n message: 'Require @mention in groups?',\n default: true,\n });\n\n return { dmPolicy, groupPolicy, allowFrom, groupAllowFrom, requireMention };\n}\n\nexport const feishuCliLoginAdapter: ChannelCliLoginAdapter = {\n async runLogin(params) {\n const { configPath, verbose, timeoutMs = 480_000, accountId, writeConfig = true } = params;\n\n if (verbose) {\n console.log(`[feishu-login] configPath=${configPath}, timeoutMs=${timeoutMs}`);\n }\n\n const config = loadConfigFromPath(configPath);\n const existingFeishu = config.channels?.feishu as Record<string, unknown> | undefined;\n const existingAppId = typeof existingFeishu?.appId === 'string' ? existingFeishu.appId : '';\n const alreadyConfigured = isFeishuConfigured(config);\n\n console.log(`\\n${'='.repeat(50)}`);\n console.log('📱 Feishu / Lark login');\n console.log(`${'='.repeat(50)}\\n`);\n\n if (alreadyConfigured) {\n const reconfigure = await confirm({\n message: `Feishu is already configured (App ID: ${existingAppId}). Reconfigure?`,\n default: false,\n });\n if (!reconfigure) {\n console.log('Keeping existing configuration.\\n');\n return { ok: true, message: 'Existing configuration kept.', accountId };\n }\n }\n\n const credentials = await acquireCredentials({\n existingAppId,\n timeoutMs,\n });\n\n const policies = await promptSecurityPolicies(credentials.ownerOpenId);\n\n const nextFeishu: Record<string, unknown> = {\n ...(existingFeishu ?? {}),\n enabled: true,\n appId: credentials.appId,\n appSecret: credentials.appSecret,\n domain: credentials.domain,\n connectionMode: (existingFeishu?.connectionMode as string) || 'websocket',\n dmPolicy: policies.dmPolicy,\n groupPolicy: policies.groupPolicy,\n allowFrom: policies.allowFrom,\n groupAllowFrom: policies.groupAllowFrom,\n requireMention: policies.requireMention,\n };\n\n const nextConfig: Config = {\n ...config,\n channels: {\n ...config.channels,\n feishu: nextFeishu,\n },\n };\n\n if (writeConfig) {\n writeConfigToPath(configPath, nextConfig);\n console.log(`✅ Feishu configuration saved to ${configPath}\\n`);\n } else {\n console.log('✅ Feishu credentials acquired (--credentials-only: config not updated).\\n');\n }\n\n return {\n ok: true,\n message: `Feishu login complete (App ID: ${credentials.appId}).`,\n accountId: accountId ?? undefined,\n };\n },\n};\n"],"mappings":";;;;;;;;;;AAyBA,SAAS,mBAAmB,YAA4B;AACtD,KAAI;EACF,MAAM,MAAMA,OAAG,aAAa,YAAY,OAAO;AAC/C,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO,EAAE;;;AAIb,SAAS,kBAAkB,YAAoB,QAAsB;AACnE,QAAG,cAAc,YAAY,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG,MAAM,OAAO;;AAG9E,SAAS,mBAAmB,QAAyB;CACnD,MAAM,SAAS,OAAO,UAAU;AAChC,KAAI,CAAC,OAAQ,QAAO;CACpB,MAAM,QAAQ,OAAO,OAAO,UAAU,WAAW,OAAO,MAAM,MAAM,GAAG;CACvE,MAAM,YAAY,OAAO,OAAO,cAAc,WAAW,OAAO,UAAU,MAAM,GAAG;AACnF,QAAO,QAAQ,SAAS,UAAU;;AAGpC,SAAS,kBAAkB,KAAqC;AAC9D,KAAI,CAAC,IAAI,MAAM,CAAE,QAAO,EAAE;AAC1B,QAAO,IACJ,MAAM,WAAW,CACjB,KAAK,UAAU,MAAM,MAAM,CAAC,CAC5B,OAAO,QAAQ,CACf,KAAK,UAAU;EACd,MAAM,WAAW,SAAS,OAAO,GAAG;AACpC,SAAO,CAAC,MAAM,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,WAAW;GACnE;;AAGN,eAAe,mBAAmB,QAQ/B;CACD,MAAM,SAAS,MAAM,OAAqB;EACxC,SAAS;EACT,SAAS,CACP;GAAE,OAAO;GAAU,MAAM;GAA2B,aAAa;GAAkB,EACnF;GAAE,OAAO;GAAQ,MAAM;GAA6B,aAAa;GAAwB,CAC1F;EACD,SAAS;EACV,CAAC;AAUF,KAPgB,MADM,oBAAoB,OAAO,GAE7C,MAAM,QAAQ;EACZ,SAAS;EACT,SAAS;EACV,CAAC,GACF,OAES;AACX,UAAQ,IAAI,2DAA2D;EACvE,MAAM,QAAQ,MAAM,qBAAqB,OAAO;AAChD,QAAM,YAAY,MAAM,MAAM;AAC9B,UAAQ,IAAI,kCAAkC;EAE9C,MAAM,cAAc,KAAK,IAAI,MAAM,aAAa,KAAK,MAAM,OAAO,YAAY,IAAK,CAAC;EACpF,MAAM,UAAU,MAAM,oBAAoB;GACxC,YAAY,MAAM;GAClB,aAAa,MAAM;GACnB;GACA,eAAe;GAChB,CAAC;AAEF,MAAI,QAAQ,WAAW,WAAW;AAChC,WAAQ,IAAI,2BAA2B,QAAQ,OAAO,OAAO,MAAM;AACnE,UAAO;IACL,OAAO,QAAQ,OAAO;IACtB,WAAW,QAAQ,OAAO;IAC1B,QAAQ,QAAQ,OAAO;IACvB,aAAa,QAAQ,OAAO;IAC7B;;EAGH,MAAM,SACJ,QAAQ,WAAW,kBACf,+BACA,QAAQ,WAAW,YACjB,qBACA,QAAQ,WAAW,YACjB,oBACA,UAAU,aAAa,UAAU,QAAQ,UAAU;AAC7D,UAAQ,IAAI,GAAG,OAAO,kCAAkC;;AAG1D,SAAQ,IAAI,mFAAmF;AAiB/F,QAAO;EAAE,QAdP,MAAM,MAAM;GACV,SAAS;GACT,SAAS,OAAO,iBAAiB,KAAA;GACjC,WAAW,UAAU,MAAM,MAAM,CAAC,SAAS,KAAK;GACjD,CAAC,EACF,MASY;EAAE,YANd,MAAM,MAAM;GACV,SAAS;GACT,WAAW,UAAU,MAAM,MAAM,CAAC,SAAS,KAAK;GACjD,CAAC,EACF,MAEuB;EAAE;EAAQ;;AAGrC,eAAe,uBAAuB,aAMnC;CACD,MAAM,WAAW,MAAM,OAAiB;EACtC,SAAS;EACT,SAAS;GACP;IAAE,OAAO;IAAW,MAAM;IAA0B,aAAa;IAAwB;GACzF;IAAE,OAAO;IAAa,MAAM;IAAa,aAAa;IAA0B;GAChF;IAAE,OAAO;IAAQ,MAAM;IAAQ,aAAa;IAAiB;GAC7D;IAAE,OAAO;IAAY,MAAM;IAAY,aAAa;IAAe;GACpE;EACD,SAAS;EACV,CAAC;CAEF,IAAI,YAAoC,EAAE;AAC1C,KAAI,aAAa,aAAa;EAC5B,MAAM,eAAe,eAAe;AAKpC,cAAY,kBAAkB,MAJZ,MAAM;GACtB,SAAS;GACT,SAAS;GACV,CAAC,IACmC,aAAa;;CAGpD,MAAM,cAAc,MAAM,OAAoB;EAC5C,SAAS;EACT,SAAS;GACP;IAAE,OAAO;IAAa,MAAM;IAA4B,aAAa;IAA2B;GAChG;IAAE,OAAO;IAAQ,MAAM;IAAQ,aAAa;IAAsB;GAClE;IAAE,OAAO;IAAY,MAAM;IAAY,aAAa;IAAkB;GACvE;EACD,SAAS;EACV,CAAC;CAEF,IAAI,iBAAyC,EAAE;AAC/C,KAAI,gBAAgB,YAKlB,kBAAiB,kBAAkB,MAJjB,MAAM;EACtB,SAAS;EACT,SAAS;EACV,CAAC,CACqC;CAGzC,MAAM,iBAAiB,MAAM,QAAQ;EACnC,SAAS;EACT,SAAS;EACV,CAAC;AAEF,QAAO;EAAE;EAAU;EAAa;EAAW;EAAgB;EAAgB;;AAG7E,MAAa,wBAAgD,EAC3D,MAAM,SAAS,QAAQ;CACrB,MAAM,EAAE,YAAY,SAAS,YAAY,MAAS,WAAW,cAAc,SAAS;AAEpF,KAAI,QACF,SAAQ,IAAI,6BAA6B,WAAW,cAAc,YAAY;CAGhF,MAAM,SAAS,mBAAmB,WAAW;CAC7C,MAAM,iBAAiB,OAAO,UAAU;CACxC,MAAM,gBAAgB,OAAO,gBAAgB,UAAU,WAAW,eAAe,QAAQ;CACzF,MAAM,oBAAoB,mBAAmB,OAAO;AAEpD,SAAQ,IAAI,KAAK,IAAI,OAAO,GAAG,GAAG;AAClC,SAAQ,IAAI,yBAAyB;AACrC,SAAQ,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,IAAI;AAElC,KAAI;MAKE,CAAC,MAJqB,QAAQ;GAChC,SAAS,yCAAyC,cAAc;GAChE,SAAS;GACV,CAAC,EACgB;AAChB,WAAQ,IAAI,oCAAoC;AAChD,UAAO;IAAE,IAAI;IAAM,SAAS;IAAgC;IAAW;;;CAI3E,MAAM,cAAc,MAAM,mBAAmB;EAC3C;EACA;EACD,CAAC;CAEF,MAAM,WAAW,MAAM,uBAAuB,YAAY,YAAY;CAEtE,MAAM,aAAsC;EAC1C,GAAI,kBAAkB,EAAE;EACxB,SAAS;EACT,OAAO,YAAY;EACnB,WAAW,YAAY;EACvB,QAAQ,YAAY;EACpB,gBAAiB,gBAAgB,kBAA6B;EAC9D,UAAU,SAAS;EACnB,aAAa,SAAS;EACtB,WAAW,SAAS;EACpB,gBAAgB,SAAS;EACzB,gBAAgB,SAAS;EAC1B;CAED,MAAM,aAAqB;EACzB,GAAG;EACH,UAAU;GACR,GAAG,OAAO;GACV,QAAQ;GACT;EACF;AAED,KAAI,aAAa;AACf,oBAAkB,YAAY,WAAW;AACzC,UAAQ,IAAI,mCAAmC,WAAW,IAAI;OAE9D,SAAQ,IAAI,4EAA4E;AAG1F,QAAO;EACL,IAAI;EACJ,SAAS,kCAAkC,YAAY,MAAM;EAC7D,WAAW,aAAa,KAAA;EACzB;GAEJ"}
@@ -1,3 +1,4 @@
1
+ import { beginAppRegistration, initAppRegistration, pollAppRegistration, printQrCode } from "../auth/app-registration.js";
1
2
  import { confirm, input, select } from "@inquirer/prompts";
2
3
  //#region extensions/feishu/src/adapters/onboard-cli.ts
3
4
  /**
@@ -5,10 +6,6 @@ import { confirm, input, select } from "@inquirer/prompts";
5
6
  *
6
7
  * This configures the single-account layout under `channels.feishu.*`.
7
8
  */
8
- const FEISHU_ACCOUNTS_URL = "https://accounts.feishu.cn";
9
- const LARK_ACCOUNTS_URL = "https://accounts.larksuite.com";
10
- const REGISTRATION_PATH = "/oauth/v1/app/registration";
11
- const REQUEST_TIMEOUT_MS = 1e4;
12
9
  function isFeishuConfigured(config) {
13
10
  const feishu = config.channels?.feishu;
14
11
  if (!feishu) return false;
@@ -23,107 +20,6 @@ function parseAllowlistRaw(raw) {
23
20
  return !isNaN(num) && String(num) === e ? num : e;
24
21
  });
25
22
  }
26
- function accountsBaseUrl(domain) {
27
- return domain === "lark" ? LARK_ACCOUNTS_URL : FEISHU_ACCOUNTS_URL;
28
- }
29
- async function postRegistration(baseUrl, body) {
30
- return await (await fetch(`${baseUrl}${REGISTRATION_PATH}`, {
31
- method: "POST",
32
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
33
- body: new URLSearchParams(body).toString(),
34
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
35
- })).json();
36
- }
37
- async function initAppRegistration(domain) {
38
- try {
39
- const res = await postRegistration(accountsBaseUrl(domain), { action: "init" });
40
- return Boolean(res.supported_auth_methods?.includes("client_secret"));
41
- } catch {
42
- return false;
43
- }
44
- }
45
- async function beginAppRegistration(domain) {
46
- const res = await postRegistration(accountsBaseUrl(domain), {
47
- action: "begin",
48
- archetype: "PersonalAgent",
49
- auth_method: "client_secret",
50
- request_user_info: "open_id"
51
- });
52
- const qrUrl = new URL(res.verification_uri_complete);
53
- qrUrl.searchParams.set("from", "xopc_onboard");
54
- qrUrl.searchParams.set("tp", "ob_cli_app");
55
- return {
56
- deviceCode: res.device_code,
57
- qrUrl: qrUrl.toString(),
58
- intervalSec: res.interval || 5,
59
- expireInSec: res.expire_in || 600
60
- };
61
- }
62
- async function sleep(ms) {
63
- await new Promise((r) => setTimeout(r, ms));
64
- }
65
- async function pollAppRegistration(params) {
66
- let domain = params.initialDomain;
67
- let intervalSec = params.intervalSec;
68
- let domainSwitched = false;
69
- const deadline = Date.now() + params.expireInSec * 1e3;
70
- while (Date.now() < deadline) {
71
- let pollRes;
72
- try {
73
- pollRes = await postRegistration(accountsBaseUrl(domain), {
74
- action: "poll",
75
- device_code: params.deviceCode,
76
- tp: "ob_cli_app"
77
- });
78
- } catch {
79
- await sleep(intervalSec * 1e3);
80
- continue;
81
- }
82
- if (pollRes.user_info?.tenant_brand) {
83
- const isLark = pollRes.user_info.tenant_brand === "lark";
84
- if (!domainSwitched && isLark) {
85
- domain = "lark";
86
- domainSwitched = true;
87
- continue;
88
- }
89
- }
90
- if (pollRes.client_id && pollRes.client_secret) return {
91
- status: "success",
92
- result: {
93
- appId: pollRes.client_id,
94
- appSecret: pollRes.client_secret,
95
- domain,
96
- openId: pollRes.user_info?.open_id
97
- }
98
- };
99
- if (pollRes.error) if (pollRes.error === "authorization_pending") {} else if (pollRes.error === "slow_down") intervalSec += 5;
100
- else if (pollRes.error === "access_denied") return { status: "access_denied" };
101
- else if (pollRes.error === "expired_token") return { status: "expired" };
102
- else return {
103
- status: "error",
104
- message: `${pollRes.error}: ${pollRes.error_description ?? "unknown"}`
105
- };
106
- await sleep(intervalSec * 1e3);
107
- }
108
- return { status: "timeout" };
109
- }
110
- async function printQrCode(url) {
111
- try {
112
- const qrcodeTerminal = await import(
113
- /* @vite-ignore */
114
- "qrcode-terminal"
115
- );
116
- await new Promise((resolve) => {
117
- qrcodeTerminal.default.generate(url, { small: true }, (qr) => {
118
- process.stdout.write(qr.endsWith("\n") ? qr : `${qr}\n`);
119
- resolve();
120
- });
121
- });
122
- } catch {
123
- console.log("Open this URL in a browser to scan:\n");
124
- console.log(url);
125
- }
126
- }
127
23
  async function configureFeishu(config) {
128
24
  console.log(`\n${"=".repeat(50)}`);
129
25
  console.log("📱 Feishu / Lark setup");
@@ -1 +1 @@
1
- {"version":3,"file":"onboard-cli.js","names":[],"sources":["../../../../../extensions/feishu/src/adapters/onboard-cli.ts"],"sourcesContent":["/**\n * Feishu interactive onboarding (CLI onboard) — {@link ChannelOnboardAdapter}.\n *\n * This configures the single-account layout under `channels.feishu.*`.\n */\n\nimport { confirm, input, select } from '@inquirer/prompts';\n\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport type { ChannelOnboardAdapter } from '@xopcai/xopc/channels/plugins/types.adapters.js';\n\ntype DmPolicy = 'pairing' | 'allowlist' | 'open' | 'disabled';\ntype GroupPolicy = 'open' | 'disabled' | 'allowlist';\ntype Domain = 'feishu' | 'lark';\ntype RenderMode = 'auto' | 'raw' | 'card';\ntype ConnectionMode = 'websocket' | 'webhook';\ntype ReactionNotifications = 'off' | 'own' | 'all';\n\ntype AppRegistrationResult = {\n appId: string;\n appSecret: string;\n domain: Domain;\n openId?: string;\n};\n\nconst FEISHU_ACCOUNTS_URL = 'https://accounts.feishu.cn';\nconst LARK_ACCOUNTS_URL = 'https://accounts.larksuite.com';\nconst REGISTRATION_PATH = '/oauth/v1/app/registration';\nconst REQUEST_TIMEOUT_MS = 10_000;\n\nfunction isFeishuConfigured(config: Config): boolean {\n const feishu = config.channels?.feishu as Record<string, unknown> | undefined;\n if (!feishu) return false;\n const appId = typeof feishu.appId === 'string' ? feishu.appId.trim() : '';\n const appSecret = typeof feishu.appSecret === 'string' ? feishu.appSecret.trim() : '';\n const enabled = feishu.enabled === true;\n return enabled && Boolean(appId && appSecret);\n}\n\nfunction parseAllowlistRaw(raw: string): Array<string | number> {\n if (!raw.trim()) return [];\n const entries = raw\n .split(/[,\\s\\n]+/)\n .map((s) => s.trim())\n .filter(Boolean);\n return entries.map((e) => {\n const num = parseInt(e, 10);\n return !isNaN(num) && String(num) === e ? num : e;\n });\n}\n\nfunction accountsBaseUrl(domain: Domain): string {\n return domain === 'lark' ? LARK_ACCOUNTS_URL : FEISHU_ACCOUNTS_URL;\n}\n\nasync function postRegistration<T>(baseUrl: string, body: Record<string, string>): Promise<T> {\n const res = await fetch(`${baseUrl}${REGISTRATION_PATH}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams(body).toString(),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n // Registration poll returns 4xx for pending/error states with a JSON body.\n return (await res.json()) as T;\n}\n\nasync function initAppRegistration(domain: Domain): Promise<boolean> {\n type InitResponse = { supported_auth_methods?: string[] };\n try {\n const res = await postRegistration<InitResponse>(accountsBaseUrl(domain), { action: 'init' });\n return Boolean(res.supported_auth_methods?.includes('client_secret'));\n } catch {\n return false;\n }\n}\n\nasync function beginAppRegistration(domain: Domain): Promise<{\n deviceCode: string;\n qrUrl: string;\n intervalSec: number;\n expireInSec: number;\n}> {\n type RawBeginResponse = {\n device_code: string;\n verification_uri_complete: string;\n interval?: number;\n expire_in?: number;\n };\n const res = await postRegistration<RawBeginResponse>(accountsBaseUrl(domain), {\n action: 'begin',\n archetype: 'PersonalAgent',\n auth_method: 'client_secret',\n request_user_info: 'open_id',\n });\n const qrUrl = new URL(res.verification_uri_complete);\n qrUrl.searchParams.set('from', 'xopc_onboard');\n qrUrl.searchParams.set('tp', 'ob_cli_app');\n return {\n deviceCode: res.device_code,\n qrUrl: qrUrl.toString(),\n intervalSec: res.interval || 5,\n expireInSec: res.expire_in || 600,\n };\n}\n\nasync function sleep(ms: number): Promise<void> {\n await new Promise<void>((r) => setTimeout(r, ms));\n}\n\nasync function pollAppRegistration(params: {\n deviceCode: string;\n intervalSec: number;\n expireInSec: number;\n initialDomain: Domain;\n}): Promise<\n | { status: 'success'; result: AppRegistrationResult }\n | { status: 'access_denied' | 'expired' | 'timeout'; message?: string }\n | { status: 'error'; message: string }\n> {\n type PollResponse = {\n client_id?: string;\n client_secret?: string;\n user_info?: { open_id?: string; tenant_brand?: 'feishu' | 'lark' };\n error?: string;\n error_description?: string;\n };\n\n let domain: Domain = params.initialDomain;\n let intervalSec = params.intervalSec;\n let domainSwitched = false;\n const deadline = Date.now() + params.expireInSec * 1000;\n\n while (Date.now() < deadline) {\n let pollRes: PollResponse;\n try {\n pollRes = await postRegistration<PollResponse>(accountsBaseUrl(domain), {\n action: 'poll',\n device_code: params.deviceCode,\n tp: 'ob_cli_app',\n });\n } catch {\n await sleep(intervalSec * 1000);\n continue;\n }\n\n // Auto-detect domain based on tenant.\n if (pollRes.user_info?.tenant_brand) {\n const isLark = pollRes.user_info.tenant_brand === 'lark';\n if (!domainSwitched && isLark) {\n domain = 'lark';\n domainSwitched = true;\n continue;\n }\n }\n\n if (pollRes.client_id && pollRes.client_secret) {\n return {\n status: 'success',\n result: {\n appId: pollRes.client_id,\n appSecret: pollRes.client_secret,\n domain,\n openId: pollRes.user_info?.open_id,\n },\n };\n }\n\n if (pollRes.error) {\n if (pollRes.error === 'authorization_pending') {\n // keep waiting\n } else if (pollRes.error === 'slow_down') {\n intervalSec += 5;\n } else if (pollRes.error === 'access_denied') {\n return { status: 'access_denied' };\n } else if (pollRes.error === 'expired_token') {\n return { status: 'expired' };\n } else {\n return {\n status: 'error',\n message: `${pollRes.error}: ${pollRes.error_description ?? 'unknown'}`,\n };\n }\n }\n\n await sleep(intervalSec * 1000);\n }\n\n return { status: 'timeout' };\n}\n\nasync function printQrCode(url: string): Promise<void> {\n try {\n const qrcodeTerminal = await import(/* @vite-ignore */ 'qrcode-terminal');\n await new Promise<void>((resolve) => {\n qrcodeTerminal.default.generate(url, { small: true }, (qr: string) => {\n process.stdout.write(qr.endsWith('\\n') ? qr : `${qr}\\n`);\n resolve();\n });\n });\n } catch {\n console.log('Open this URL in a browser to scan:\\n');\n console.log(url);\n }\n}\n\nasync function configureFeishu(config: Config): Promise<Config> {\n console.log(`\\n${'='.repeat(50)}`);\n console.log('📱 Feishu / Lark setup');\n console.log(`${'='.repeat(50)}\\n`);\n\n const existing = config.channels?.feishu as Record<string, unknown> | undefined;\n const existingAppId = typeof existing?.appId === 'string' ? existing.appId : '';\n const existingDomain = typeof existing?.domain === 'string' ? existing.domain : '';\n\n if (existing?.enabled === true && existingAppId) {\n const keep = await confirm({\n message: 'A Feishu config already exists. Reconfigure it?',\n default: false,\n });\n if (!keep) return config;\n }\n\n const initialDomain = await select<Domain>({\n message: 'Feishu/Lark domain:',\n choices: [\n { value: 'feishu', name: 'feishu (open.feishu.cn)', description: 'China / Feishu' },\n { value: 'lark', name: 'lark (open.larksuite.com)', description: 'International / Lark' },\n ],\n default: existingDomain === 'lark' ? 'lark' : 'feishu',\n });\n\n const canScanToCreate = await initAppRegistration(initialDomain);\n const useScanToCreate = canScanToCreate\n ? await confirm({\n message: 'Create an app by scanning a QR code (recommended)?',\n default: true,\n })\n : false;\n\n let appId = '';\n let appSecret = '';\n let domain: Domain = initialDomain;\n let ownerOpenId: string | undefined;\n\n if (useScanToCreate) {\n console.log('\\nScan this QR code with Feishu/Lark to create an app:\\n');\n const begin = await beginAppRegistration(initialDomain);\n await printQrCode(begin.qrUrl);\n console.log('\\nWaiting for confirmation...\\n');\n const outcome = await pollAppRegistration({\n deviceCode: begin.deviceCode,\n intervalSec: begin.intervalSec,\n expireInSec: begin.expireInSec,\n initialDomain,\n });\n if (outcome.status !== 'success') {\n console.log('Scan-to-create did not complete. Falling back to manual input.\\n');\n } else {\n appId = outcome.result.appId;\n appSecret = outcome.result.appSecret;\n domain = outcome.result.domain;\n ownerOpenId = outcome.result.openId;\n console.log(`✅ App created. Domain detected as \"${domain}\".\\n`);\n }\n }\n\n if (!appId || !appSecret) {\n console.log('📝 Feishu app credentials (from Feishu Open Platform developer console):\\n');\n\n appId = (await input({\n message: 'App ID (cli_xxx):',\n default: existingAppId || undefined,\n validate: (v) => v.trim().length > 0 || 'App ID cannot be empty',\n })).trim();\n\n appSecret = (await input({\n message: 'App Secret:',\n validate: (v) => v.trim().length > 0 || 'App Secret cannot be empty',\n })).trim();\n\n domain = initialDomain;\n }\n\n const connectionMode = await select<ConnectionMode>({\n message: 'Connection mode:',\n choices: [\n { value: 'websocket', name: 'websocket [recommended]', description: 'Socket Mode (persistent connection)' },\n { value: 'webhook', name: 'webhook', description: 'Local HTTP server receives events' },\n ],\n default: 'websocket',\n });\n\n let verificationToken: string | undefined;\n let encryptKey: string | undefined;\n let webhookHost: string | undefined;\n let webhookPort: number | undefined;\n let webhookPath: string | undefined;\n if (connectionMode === 'webhook') {\n console.log('\\n🪝 Webhook secrets (from Feishu event subscription settings):\\n');\n verificationToken = (await input({\n message: 'Verification Token:',\n validate: (v) => v.trim().length > 0 || 'Verification Token cannot be empty',\n })).trim();\n encryptKey = (await input({\n message: 'Encrypt Key:',\n validate: (v) => v.trim().length > 0 || 'Encrypt Key cannot be empty',\n })).trim();\n webhookHost = (await input({\n message: 'Webhook host:',\n default: typeof existing?.webhookHost === 'string' ? existing.webhookHost : '127.0.0.1',\n })).trim();\n webhookPort = Number(\n (await input({\n message: 'Webhook port:',\n default: typeof existing?.webhookPort === 'number' ? String(existing.webhookPort) : '3000',\n })).trim(),\n );\n webhookPath = (await input({\n message: 'Webhook path:',\n default: typeof existing?.webhookPath === 'string' ? existing.webhookPath : '/feishu/events',\n })).trim();\n }\n\n const dmPolicy = await select<DmPolicy>({\n message: 'DM (private chat) policy:',\n choices: [\n { value: 'pairing', name: 'pairing [recommended]', description: 'New users must /pair before chatting' },\n { value: 'allowlist', name: 'allowlist', description: 'Only allowlisted users can DM' },\n { value: 'open', name: 'open', description: 'Anyone can DM (not recommended)' },\n { value: 'disabled', name: 'disabled', description: 'Disable DMs' },\n ],\n default: 'pairing',\n });\n\n let allowFrom: Array<string | number> | undefined;\n if (dmPolicy === 'allowlist') {\n const defaultAllow =\n ownerOpenId && (existing?.allowFrom == null || (Array.isArray(existing.allowFrom) && existing.allowFrom.length === 0))\n ? ownerOpenId\n : '';\n const raw = await input({\n message: 'Allowed user open_id / union_id / numeric ids (comma-separated):',\n default: defaultAllow,\n });\n allowFrom = parseAllowlistRaw(raw || defaultAllow);\n }\n\n const groupPolicy = await select<GroupPolicy>({\n message: 'Group chat policy:',\n choices: [\n { value: 'allowlist', name: 'allowlist [recommended]', description: 'Only allowlisted groups can use the bot' },\n { value: 'open', name: 'open', description: 'All groups allowed' },\n { value: 'disabled', name: 'disabled', description: 'Disable groups' },\n ],\n default: 'allowlist',\n });\n\n let groupAllowFrom: Array<string | number> | undefined;\n if (groupPolicy === 'allowlist') {\n const raw = await input({\n message: 'Allowed group chat IDs (comma-separated, e.g. oc_xxx):',\n default: '',\n });\n groupAllowFrom = parseAllowlistRaw(raw);\n }\n\n const requireMention = await confirm({\n message: 'Require @mention in groups?',\n default: true,\n });\n\n const renderMode = await select<RenderMode>({\n message: 'Default render mode:',\n choices: [\n { value: 'auto', name: 'auto', description: 'Let xopc decide (default)' },\n { value: 'raw', name: 'raw', description: 'Send plain text only' },\n { value: 'card', name: 'card', description: 'Prefer interactive cards (CardKit streaming)' },\n ],\n default: 'auto',\n });\n\n const streaming = await confirm({\n message: 'Enable streaming updates (Thinking… + incremental output)?',\n default: false,\n });\n\n const reactionNotifications = await select<ReactionNotifications>({\n message: 'Reaction notifications:',\n choices: [\n { value: 'off', name: 'off', description: 'Disable reaction notifications' },\n { value: 'own', name: 'own [recommended]', description: 'Only notify reactions to bot messages' },\n { value: 'all', name: 'all', description: 'Notify all reactions (noisy)' },\n ],\n default: 'own',\n });\n\n const enableFeishuTools = await select<'minimal' | 'docs' | 'full'>({\n message: 'Enable Feishu tools (docs/wiki/drive/etc.)?',\n choices: [\n { value: 'minimal', name: 'minimal', description: 'No extra tools (chat only)' },\n { value: 'docs', name: 'docs', description: 'Enable doc/wiki/drive/scopes (common)' },\n { value: 'full', name: 'full', description: 'Enable doc/wiki/drive/bitable/perm/scopes' },\n ],\n default: 'docs',\n });\n\n const tools =\n enableFeishuTools === 'minimal'\n ? { doc: false, wiki: false, drive: false, perm: false, bitable: false, scopes: true }\n : enableFeishuTools === 'docs'\n ? { doc: true, wiki: true, drive: true, perm: false, bitable: true, scopes: true }\n : { doc: true, wiki: true, drive: true, perm: true, bitable: true, scopes: true };\n\n const nextFeishu: Record<string, unknown> = {\n ...(existing ?? {}),\n enabled: true,\n appId,\n appSecret,\n domain,\n connectionMode,\n ...(connectionMode === 'webhook'\n ? {\n verificationToken,\n encryptKey,\n webhookHost,\n webhookPort,\n webhookPath,\n }\n : {}),\n dmPolicy,\n groupPolicy,\n allowFrom: allowFrom ?? (existing?.allowFrom as any) ?? [],\n groupAllowFrom: groupAllowFrom ?? (existing?.groupAllowFrom as any) ?? [],\n requireMention,\n renderMode,\n streaming,\n reactionNotifications,\n actions: { reactions: true },\n tools,\n historyLimit: typeof existing?.historyLimit === 'number' ? existing.historyLimit : 50,\n textChunkLimit: typeof existing?.textChunkLimit === 'number' ? existing.textChunkLimit : 4000,\n };\n\n const newConfig: Config = {\n ...config,\n channels: {\n ...config.channels,\n feishu: nextFeishu,\n },\n };\n\n console.log('\\n✅ Feishu configuration complete\\n');\n return newConfig;\n}\n\nexport const feishuOnboardAdapter: ChannelOnboardAdapter = {\n isConfigured: isFeishuConfigured,\n configure: configureFeishu,\n};\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"onboard-cli.js","names":[],"sources":["../../../../../extensions/feishu/src/adapters/onboard-cli.ts"],"sourcesContent":["/**\n * Feishu interactive onboarding (CLI onboard) — {@link ChannelOnboardAdapter}.\n *\n * This configures the single-account layout under `channels.feishu.*`.\n */\n\nimport { confirm, input, select } from '@inquirer/prompts';\n\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport type { ChannelOnboardAdapter } from '@xopcai/xopc/channels/plugins/types.adapters.js';\n\nimport {\n initAppRegistration,\n beginAppRegistration,\n pollAppRegistration,\n printQrCode,\n type FeishuDomain,\n} from '../auth/app-registration.js';\n\ntype DmPolicy = 'pairing' | 'allowlist' | 'open' | 'disabled';\ntype GroupPolicy = 'open' | 'disabled' | 'allowlist';\ntype RenderMode = 'auto' | 'raw' | 'card';\ntype ConnectionMode = 'websocket' | 'webhook';\ntype ReactionNotifications = 'off' | 'own' | 'all';\n\nfunction isFeishuConfigured(config: Config): boolean {\n const feishu = config.channels?.feishu as Record<string, unknown> | undefined;\n if (!feishu) return false;\n const appId = typeof feishu.appId === 'string' ? feishu.appId.trim() : '';\n const appSecret = typeof feishu.appSecret === 'string' ? feishu.appSecret.trim() : '';\n const enabled = feishu.enabled === true;\n return enabled && Boolean(appId && appSecret);\n}\n\nfunction parseAllowlistRaw(raw: string): Array<string | number> {\n if (!raw.trim()) return [];\n const entries = raw\n .split(/[,\\s\\n]+/)\n .map((s) => s.trim())\n .filter(Boolean);\n return entries.map((e) => {\n const num = parseInt(e, 10);\n return !isNaN(num) && String(num) === e ? num : e;\n });\n}\n\nasync function configureFeishu(config: Config): Promise<Config> {\n console.log(`\\n${'='.repeat(50)}`);\n console.log('📱 Feishu / Lark setup');\n console.log(`${'='.repeat(50)}\\n`);\n\n const existing = config.channels?.feishu as Record<string, unknown> | undefined;\n const existingAppId = typeof existing?.appId === 'string' ? existing.appId : '';\n const existingDomain = typeof existing?.domain === 'string' ? existing.domain : '';\n\n if (existing?.enabled === true && existingAppId) {\n const keep = await confirm({\n message: 'A Feishu config already exists. Reconfigure it?',\n default: false,\n });\n if (!keep) return config;\n }\n\n const initialDomain = await select<FeishuDomain>({\n message: 'Feishu/Lark domain:',\n choices: [\n { value: 'feishu', name: 'feishu (open.feishu.cn)', description: 'China / Feishu' },\n { value: 'lark', name: 'lark (open.larksuite.com)', description: 'International / Lark' },\n ],\n default: existingDomain === 'lark' ? 'lark' : 'feishu',\n });\n\n const canScanToCreate = await initAppRegistration(initialDomain);\n const useScanToCreate = canScanToCreate\n ? await confirm({\n message: 'Create an app by scanning a QR code (recommended)?',\n default: true,\n })\n : false;\n\n let appId = '';\n let appSecret = '';\n let domain: FeishuDomain = initialDomain;\n let ownerOpenId: string | undefined;\n\n if (useScanToCreate) {\n console.log('\\nScan this QR code with Feishu/Lark to create an app:\\n');\n const begin = await beginAppRegistration(initialDomain);\n await printQrCode(begin.qrUrl);\n console.log('\\nWaiting for confirmation...\\n');\n const outcome = await pollAppRegistration({\n deviceCode: begin.deviceCode,\n intervalSec: begin.intervalSec,\n expireInSec: begin.expireInSec,\n initialDomain,\n });\n if (outcome.status !== 'success') {\n console.log('Scan-to-create did not complete. Falling back to manual input.\\n');\n } else {\n appId = outcome.result.appId;\n appSecret = outcome.result.appSecret;\n domain = outcome.result.domain;\n ownerOpenId = outcome.result.openId;\n console.log(`✅ App created. Domain detected as \"${domain}\".\\n`);\n }\n }\n\n if (!appId || !appSecret) {\n console.log('📝 Feishu app credentials (from Feishu Open Platform developer console):\\n');\n\n appId = (await input({\n message: 'App ID (cli_xxx):',\n default: existingAppId || undefined,\n validate: (v) => v.trim().length > 0 || 'App ID cannot be empty',\n })).trim();\n\n appSecret = (await input({\n message: 'App Secret:',\n validate: (v) => v.trim().length > 0 || 'App Secret cannot be empty',\n })).trim();\n\n domain = initialDomain;\n }\n\n const connectionMode = await select<ConnectionMode>({\n message: 'Connection mode:',\n choices: [\n { value: 'websocket', name: 'websocket [recommended]', description: 'Socket Mode (persistent connection)' },\n { value: 'webhook', name: 'webhook', description: 'Local HTTP server receives events' },\n ],\n default: 'websocket',\n });\n\n let verificationToken: string | undefined;\n let encryptKey: string | undefined;\n let webhookHost: string | undefined;\n let webhookPort: number | undefined;\n let webhookPath: string | undefined;\n if (connectionMode === 'webhook') {\n console.log('\\n🪝 Webhook secrets (from Feishu event subscription settings):\\n');\n verificationToken = (await input({\n message: 'Verification Token:',\n validate: (v) => v.trim().length > 0 || 'Verification Token cannot be empty',\n })).trim();\n encryptKey = (await input({\n message: 'Encrypt Key:',\n validate: (v) => v.trim().length > 0 || 'Encrypt Key cannot be empty',\n })).trim();\n webhookHost = (await input({\n message: 'Webhook host:',\n default: typeof existing?.webhookHost === 'string' ? existing.webhookHost : '127.0.0.1',\n })).trim();\n webhookPort = Number(\n (await input({\n message: 'Webhook port:',\n default: typeof existing?.webhookPort === 'number' ? String(existing.webhookPort) : '3000',\n })).trim(),\n );\n webhookPath = (await input({\n message: 'Webhook path:',\n default: typeof existing?.webhookPath === 'string' ? existing.webhookPath : '/feishu/events',\n })).trim();\n }\n\n const dmPolicy = await select<DmPolicy>({\n message: 'DM (private chat) policy:',\n choices: [\n { value: 'pairing', name: 'pairing [recommended]', description: 'New users must /pair before chatting' },\n { value: 'allowlist', name: 'allowlist', description: 'Only allowlisted users can DM' },\n { value: 'open', name: 'open', description: 'Anyone can DM (not recommended)' },\n { value: 'disabled', name: 'disabled', description: 'Disable DMs' },\n ],\n default: 'pairing',\n });\n\n let allowFrom: Array<string | number> | undefined;\n if (dmPolicy === 'allowlist') {\n const defaultAllow =\n ownerOpenId && (existing?.allowFrom == null || (Array.isArray(existing.allowFrom) && existing.allowFrom.length === 0))\n ? ownerOpenId\n : '';\n const raw = await input({\n message: 'Allowed user open_id / union_id / numeric ids (comma-separated):',\n default: defaultAllow,\n });\n allowFrom = parseAllowlistRaw(raw || defaultAllow);\n }\n\n const groupPolicy = await select<GroupPolicy>({\n message: 'Group chat policy:',\n choices: [\n { value: 'allowlist', name: 'allowlist [recommended]', description: 'Only allowlisted groups can use the bot' },\n { value: 'open', name: 'open', description: 'All groups allowed' },\n { value: 'disabled', name: 'disabled', description: 'Disable groups' },\n ],\n default: 'allowlist',\n });\n\n let groupAllowFrom: Array<string | number> | undefined;\n if (groupPolicy === 'allowlist') {\n const raw = await input({\n message: 'Allowed group chat IDs (comma-separated, e.g. oc_xxx):',\n default: '',\n });\n groupAllowFrom = parseAllowlistRaw(raw);\n }\n\n const requireMention = await confirm({\n message: 'Require @mention in groups?',\n default: true,\n });\n\n const renderMode = await select<RenderMode>({\n message: 'Default render mode:',\n choices: [\n { value: 'auto', name: 'auto', description: 'Let xopc decide (default)' },\n { value: 'raw', name: 'raw', description: 'Send plain text only' },\n { value: 'card', name: 'card', description: 'Prefer interactive cards (CardKit streaming)' },\n ],\n default: 'auto',\n });\n\n const streaming = await confirm({\n message: 'Enable streaming updates (Thinking… + incremental output)?',\n default: false,\n });\n\n const reactionNotifications = await select<ReactionNotifications>({\n message: 'Reaction notifications:',\n choices: [\n { value: 'off', name: 'off', description: 'Disable reaction notifications' },\n { value: 'own', name: 'own [recommended]', description: 'Only notify reactions to bot messages' },\n { value: 'all', name: 'all', description: 'Notify all reactions (noisy)' },\n ],\n default: 'own',\n });\n\n const enableFeishuTools = await select<'minimal' | 'docs' | 'full'>({\n message: 'Enable Feishu tools (docs/wiki/drive/etc.)?',\n choices: [\n { value: 'minimal', name: 'minimal', description: 'No extra tools (chat only)' },\n { value: 'docs', name: 'docs', description: 'Enable doc/wiki/drive/scopes (common)' },\n { value: 'full', name: 'full', description: 'Enable doc/wiki/drive/bitable/perm/scopes' },\n ],\n default: 'docs',\n });\n\n const tools =\n enableFeishuTools === 'minimal'\n ? { doc: false, wiki: false, drive: false, perm: false, bitable: false, scopes: true }\n : enableFeishuTools === 'docs'\n ? { doc: true, wiki: true, drive: true, perm: false, bitable: true, scopes: true }\n : { doc: true, wiki: true, drive: true, perm: true, bitable: true, scopes: true };\n\n const nextFeishu: Record<string, unknown> = {\n ...(existing ?? {}),\n enabled: true,\n appId,\n appSecret,\n domain,\n connectionMode,\n ...(connectionMode === 'webhook'\n ? {\n verificationToken,\n encryptKey,\n webhookHost,\n webhookPort,\n webhookPath,\n }\n : {}),\n dmPolicy,\n groupPolicy,\n allowFrom: allowFrom ?? (existing?.allowFrom as any) ?? [],\n groupAllowFrom: groupAllowFrom ?? (existing?.groupAllowFrom as any) ?? [],\n requireMention,\n renderMode,\n streaming,\n reactionNotifications,\n actions: { reactions: true },\n tools,\n historyLimit: typeof existing?.historyLimit === 'number' ? existing.historyLimit : 50,\n textChunkLimit: typeof existing?.textChunkLimit === 'number' ? existing.textChunkLimit : 4000,\n };\n\n const newConfig: Config = {\n ...config,\n channels: {\n ...config.channels,\n feishu: nextFeishu,\n },\n };\n\n console.log('\\n✅ Feishu configuration complete\\n');\n return newConfig;\n}\n\nexport const feishuOnboardAdapter: ChannelOnboardAdapter = {\n isConfigured: isFeishuConfigured,\n configure: configureFeishu,\n};\n\n"],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Feishu/Lark App Registration — QR scan-to-create and OAuth device flow.
3
+ *
4
+ * Shared between `cli-login.ts` (channels login) and `onboard-cli.ts` (onboard wizard).
5
+ */
6
+ export type FeishuDomain = 'feishu' | 'lark';
7
+ export type AppRegistrationResult = {
8
+ appId: string;
9
+ appSecret: string;
10
+ domain: FeishuDomain;
11
+ openId?: string;
12
+ };
13
+ /**
14
+ * Check if the Feishu/Lark app registration endpoint supports scan-to-create.
15
+ */
16
+ export declare function initAppRegistration(domain: FeishuDomain): Promise<boolean>;
17
+ /**
18
+ * Start the device-code flow and return a QR URL for the user to scan.
19
+ */
20
+ export declare function beginAppRegistration(domain: FeishuDomain): Promise<{
21
+ deviceCode: string;
22
+ qrUrl: string;
23
+ intervalSec: number;
24
+ expireInSec: number;
25
+ }>;
26
+ /**
27
+ * Poll the registration endpoint until the user scans and confirms.
28
+ */
29
+ export declare function pollAppRegistration(params: {
30
+ deviceCode: string;
31
+ intervalSec: number;
32
+ expireInSec: number;
33
+ initialDomain: FeishuDomain;
34
+ }): Promise<{
35
+ status: 'success';
36
+ result: AppRegistrationResult;
37
+ } | {
38
+ status: 'access_denied' | 'expired' | 'timeout';
39
+ message?: string;
40
+ } | {
41
+ status: 'error';
42
+ message: string;
43
+ }>;
44
+ /**
45
+ * Print a QR code to the terminal (falls back to URL if qrcode-terminal is unavailable).
46
+ */
47
+ export declare function printQrCode(url: string): Promise<void>;
@@ -0,0 +1,122 @@
1
+ //#region extensions/feishu/src/auth/app-registration.ts
2
+ const FEISHU_ACCOUNTS_URL = "https://accounts.feishu.cn";
3
+ const LARK_ACCOUNTS_URL = "https://accounts.larksuite.com";
4
+ const REGISTRATION_PATH = "/oauth/v1/app/registration";
5
+ const REQUEST_TIMEOUT_MS = 1e4;
6
+ function accountsBaseUrl(domain) {
7
+ return domain === "lark" ? LARK_ACCOUNTS_URL : FEISHU_ACCOUNTS_URL;
8
+ }
9
+ async function postRegistration(baseUrl, body) {
10
+ return await (await fetch(`${baseUrl}${REGISTRATION_PATH}`, {
11
+ method: "POST",
12
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
13
+ body: new URLSearchParams(body).toString(),
14
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
15
+ })).json();
16
+ }
17
+ /**
18
+ * Check if the Feishu/Lark app registration endpoint supports scan-to-create.
19
+ */
20
+ async function initAppRegistration(domain) {
21
+ try {
22
+ const result = await postRegistration(accountsBaseUrl(domain), { action: "init" });
23
+ return Boolean(result.supported_auth_methods?.includes("client_secret"));
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ /**
29
+ * Start the device-code flow and return a QR URL for the user to scan.
30
+ */
31
+ async function beginAppRegistration(domain) {
32
+ const result = await postRegistration(accountsBaseUrl(domain), {
33
+ action: "begin",
34
+ archetype: "PersonalAgent",
35
+ auth_method: "client_secret",
36
+ request_user_info: "open_id"
37
+ });
38
+ const qrUrl = new URL(result.verification_uri_complete);
39
+ qrUrl.searchParams.set("from", "xopc_onboard");
40
+ qrUrl.searchParams.set("tp", "ob_cli_app");
41
+ return {
42
+ deviceCode: result.device_code,
43
+ qrUrl: qrUrl.toString(),
44
+ intervalSec: result.interval || 5,
45
+ expireInSec: result.expire_in || 600
46
+ };
47
+ }
48
+ /**
49
+ * Poll the registration endpoint until the user scans and confirms.
50
+ */
51
+ async function pollAppRegistration(params) {
52
+ let domain = params.initialDomain;
53
+ let intervalSec = params.intervalSec;
54
+ let domainSwitched = false;
55
+ const deadline = Date.now() + params.expireInSec * 1e3;
56
+ while (Date.now() < deadline) {
57
+ let pollResponse;
58
+ try {
59
+ pollResponse = await postRegistration(accountsBaseUrl(domain), {
60
+ action: "poll",
61
+ device_code: params.deviceCode,
62
+ tp: "ob_cli_app"
63
+ });
64
+ } catch {
65
+ await sleep(intervalSec * 1e3);
66
+ continue;
67
+ }
68
+ if (pollResponse.user_info?.tenant_brand) {
69
+ const isLark = pollResponse.user_info.tenant_brand === "lark";
70
+ if (!domainSwitched && isLark) {
71
+ domain = "lark";
72
+ domainSwitched = true;
73
+ continue;
74
+ }
75
+ }
76
+ if (pollResponse.client_id && pollResponse.client_secret) return {
77
+ status: "success",
78
+ result: {
79
+ appId: pollResponse.client_id,
80
+ appSecret: pollResponse.client_secret,
81
+ domain,
82
+ openId: pollResponse.user_info?.open_id
83
+ }
84
+ };
85
+ if (pollResponse.error) if (pollResponse.error === "authorization_pending") {} else if (pollResponse.error === "slow_down") intervalSec += 5;
86
+ else if (pollResponse.error === "access_denied") return { status: "access_denied" };
87
+ else if (pollResponse.error === "expired_token") return { status: "expired" };
88
+ else return {
89
+ status: "error",
90
+ message: `${pollResponse.error}: ${pollResponse.error_description ?? "unknown"}`
91
+ };
92
+ await sleep(intervalSec * 1e3);
93
+ }
94
+ return { status: "timeout" };
95
+ }
96
+ /**
97
+ * Print a QR code to the terminal (falls back to URL if qrcode-terminal is unavailable).
98
+ */
99
+ async function printQrCode(url) {
100
+ try {
101
+ const qrcodeTerminal = await import(
102
+ /* @vite-ignore */
103
+ "qrcode-terminal"
104
+ );
105
+ await new Promise((resolve) => {
106
+ qrcodeTerminal.default.generate(url, { small: true }, (qr) => {
107
+ process.stdout.write(qr.endsWith("\n") ? qr : `${qr}\n`);
108
+ resolve();
109
+ });
110
+ });
111
+ } catch {
112
+ console.log("Open this URL in a browser to scan:\n");
113
+ console.log(url);
114
+ }
115
+ }
116
+ function sleep(ms) {
117
+ return new Promise((resolve) => setTimeout(resolve, ms));
118
+ }
119
+ //#endregion
120
+ export { beginAppRegistration, initAppRegistration, pollAppRegistration, printQrCode };
121
+
122
+ //# sourceMappingURL=app-registration.js.map