@dsh-enhanced/lark-channel 0.1.48 → 0.1.53

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 (205) hide show
  1. package/README.md +20 -8
  2. package/docs/rsi-setup.md +145 -12
  3. package/docs/setup.md +29 -14
  4. package/docs/supervised-growth.md +2 -0
  5. package/lib/adapter.d.ts +6 -0
  6. package/lib/adapter.d.ts.map +1 -1
  7. package/lib/adapter.js +79 -8
  8. package/lib/adapter.js.map +1 -1
  9. package/lib/business-cli-install.d.ts +9 -0
  10. package/lib/business-cli-install.d.ts.map +1 -0
  11. package/lib/business-cli-install.js +367 -0
  12. package/lib/business-cli-install.js.map +1 -0
  13. package/lib/business-environment.d.ts +2 -0
  14. package/lib/business-environment.d.ts.map +1 -0
  15. package/lib/business-environment.js +16 -0
  16. package/lib/business-environment.js.map +1 -0
  17. package/lib/business-skill.d.ts +32 -0
  18. package/lib/business-skill.d.ts.map +1 -0
  19. package/lib/business-skill.js +202 -0
  20. package/lib/business-skill.js.map +1 -0
  21. package/lib/launchd-control.d.ts +24 -0
  22. package/lib/launchd-control.d.ts.map +1 -0
  23. package/lib/launchd-control.js +68 -0
  24. package/lib/launchd-control.js.map +1 -0
  25. package/lib/launchd-identity.d.ts +34 -0
  26. package/lib/launchd-identity.d.ts.map +1 -0
  27. package/lib/launchd-identity.js +184 -0
  28. package/lib/launchd-identity.js.map +1 -0
  29. package/lib/launchd-target-lock.d.ts +6 -0
  30. package/lib/launchd-target-lock.d.ts.map +1 -0
  31. package/lib/launchd-target-lock.js +48 -0
  32. package/lib/launchd-target-lock.js.map +1 -0
  33. package/lib/launchd.d.ts +18 -0
  34. package/lib/launchd.d.ts.map +1 -1
  35. package/lib/launchd.js +211 -54
  36. package/lib/launchd.js.map +1 -1
  37. package/lib/rsi-authority-config.d.ts +29 -0
  38. package/lib/rsi-authority-config.d.ts.map +1 -0
  39. package/lib/rsi-authority-config.js +233 -0
  40. package/lib/rsi-authority-config.js.map +1 -0
  41. package/lib/rsi-authority-resources.d.ts +36 -0
  42. package/lib/rsi-authority-resources.d.ts.map +1 -0
  43. package/lib/rsi-authority-resources.js +139 -0
  44. package/lib/rsi-authority-resources.js.map +1 -0
  45. package/lib/rsi-authority-runtime.d.ts +50 -0
  46. package/lib/rsi-authority-runtime.d.ts.map +1 -0
  47. package/lib/rsi-authority-runtime.js +393 -0
  48. package/lib/rsi-authority-runtime.js.map +1 -0
  49. package/lib/rsi-bootstrap-manifest.d.ts +39 -0
  50. package/lib/rsi-bootstrap-manifest.d.ts.map +1 -0
  51. package/lib/rsi-bootstrap-manifest.js +345 -0
  52. package/lib/rsi-bootstrap-manifest.js.map +1 -0
  53. package/lib/rsi-bootstrap.d.ts +30 -0
  54. package/lib/rsi-bootstrap.d.ts.map +1 -0
  55. package/lib/rsi-bootstrap.js +363 -0
  56. package/lib/rsi-bootstrap.js.map +1 -0
  57. package/lib/rsi-build.d.ts +54 -0
  58. package/lib/rsi-build.d.ts.map +1 -0
  59. package/lib/rsi-build.js +478 -0
  60. package/lib/rsi-build.js.map +1 -0
  61. package/lib/rsi-creation-build.d.ts +48 -0
  62. package/lib/rsi-creation-build.d.ts.map +1 -0
  63. package/lib/rsi-creation-build.js +363 -0
  64. package/lib/rsi-creation-build.js.map +1 -0
  65. package/lib/rsi-host-authorities.d.ts +18 -0
  66. package/lib/rsi-host-authorities.d.ts.map +1 -0
  67. package/lib/rsi-host-authorities.js +129 -0
  68. package/lib/rsi-host-authorities.js.map +1 -0
  69. package/lib/rsi-host-update.d.ts +113 -0
  70. package/lib/rsi-host-update.d.ts.map +1 -0
  71. package/lib/rsi-host-update.js +316 -0
  72. package/lib/rsi-host-update.js.map +1 -0
  73. package/lib/rsi-install-inputs.d.ts +43 -0
  74. package/lib/rsi-install-inputs.d.ts.map +1 -0
  75. package/lib/rsi-install-inputs.js +342 -0
  76. package/lib/rsi-install-inputs.js.map +1 -0
  77. package/lib/rsi-install.d.ts +54 -0
  78. package/lib/rsi-install.d.ts.map +1 -0
  79. package/lib/rsi-install.js +404 -0
  80. package/lib/rsi-install.js.map +1 -0
  81. package/lib/rsi-local-activation.d.ts +78 -0
  82. package/lib/rsi-local-activation.d.ts.map +1 -0
  83. package/lib/rsi-local-activation.js +485 -0
  84. package/lib/rsi-local-activation.js.map +1 -0
  85. package/lib/rsi-local-cohort.d.ts +70 -0
  86. package/lib/rsi-local-cohort.d.ts.map +1 -0
  87. package/lib/rsi-local-cohort.js +781 -0
  88. package/lib/rsi-local-cohort.js.map +1 -0
  89. package/lib/rsi-local-install.d.ts +68 -0
  90. package/lib/rsi-local-install.d.ts.map +1 -0
  91. package/lib/rsi-local-install.js +322 -0
  92. package/lib/rsi-local-install.js.map +1 -0
  93. package/lib/rsi-local-pair-install.d.ts +38 -0
  94. package/lib/rsi-local-pair-install.d.ts.map +1 -0
  95. package/lib/rsi-local-pair-install.js +200 -0
  96. package/lib/rsi-local-pair-install.js.map +1 -0
  97. package/lib/rsi-local-profile-update.d.ts +25 -0
  98. package/lib/rsi-local-profile-update.d.ts.map +1 -0
  99. package/lib/rsi-local-profile-update.js +226 -0
  100. package/lib/rsi-local-profile-update.js.map +1 -0
  101. package/lib/rsi-local-resources.d.ts +37 -0
  102. package/lib/rsi-local-resources.d.ts.map +1 -0
  103. package/lib/rsi-local-resources.js +184 -0
  104. package/lib/rsi-local-resources.js.map +1 -0
  105. package/lib/rsi-local-roots-extension.d.ts +11 -0
  106. package/lib/rsi-local-roots-extension.d.ts.map +1 -0
  107. package/lib/rsi-local-roots-extension.js +31 -0
  108. package/lib/rsi-local-roots-extension.js.map +1 -0
  109. package/lib/rsi-local-update-build.d.ts +18 -0
  110. package/lib/rsi-local-update-build.d.ts.map +1 -0
  111. package/lib/rsi-local-update-build.js +37 -0
  112. package/lib/rsi-local-update-build.js.map +1 -0
  113. package/lib/rsi-local-update.d.ts +39 -0
  114. package/lib/rsi-local-update.d.ts.map +1 -0
  115. package/lib/rsi-local-update.js +205 -0
  116. package/lib/rsi-local-update.js.map +1 -0
  117. package/lib/rsi-memory-learning.d.ts +30 -0
  118. package/lib/rsi-memory-learning.d.ts.map +1 -0
  119. package/lib/rsi-memory-learning.js +104 -0
  120. package/lib/rsi-memory-learning.js.map +1 -0
  121. package/lib/rsi-native-install.d.ts +38 -0
  122. package/lib/rsi-native-install.d.ts.map +1 -0
  123. package/lib/rsi-native-install.js +254 -0
  124. package/lib/rsi-native-install.js.map +1 -0
  125. package/lib/rsi-native-launchd.d.ts +7 -0
  126. package/lib/rsi-native-launchd.d.ts.map +1 -0
  127. package/lib/rsi-native-launchd.js +49 -0
  128. package/lib/rsi-native-launchd.js.map +1 -0
  129. package/lib/rsi-native-profile.d.ts +64 -0
  130. package/lib/rsi-native-profile.d.ts.map +1 -0
  131. package/lib/rsi-native-profile.js +283 -0
  132. package/lib/rsi-native-profile.js.map +1 -0
  133. package/lib/rsi-native-runtime-bootstrap.d.ts +35 -0
  134. package/lib/rsi-native-runtime-bootstrap.d.ts.map +1 -0
  135. package/lib/rsi-native-runtime-bootstrap.js +385 -0
  136. package/lib/rsi-native-runtime-bootstrap.js.map +1 -0
  137. package/lib/rsi-owner-profile.d.ts +37 -0
  138. package/lib/rsi-owner-profile.d.ts.map +1 -0
  139. package/lib/rsi-owner-profile.js +178 -0
  140. package/lib/rsi-owner-profile.js.map +1 -0
  141. package/lib/rsi-plugin-creation.d.ts +32 -0
  142. package/lib/rsi-plugin-creation.d.ts.map +1 -0
  143. package/lib/rsi-plugin-creation.js +189 -0
  144. package/lib/rsi-plugin-creation.js.map +1 -0
  145. package/lib/rsi-profile.d.ts +23 -0
  146. package/lib/rsi-profile.d.ts.map +1 -1
  147. package/lib/rsi-profile.js +154 -43
  148. package/lib/rsi-profile.js.map +1 -1
  149. package/lib/rsi-release-build.d.ts +41 -0
  150. package/lib/rsi-release-build.d.ts.map +1 -0
  151. package/lib/rsi-release-build.js +223 -0
  152. package/lib/rsi-release-build.js.map +1 -0
  153. package/lib/rsi-service-environment.d.ts +6 -0
  154. package/lib/rsi-service-environment.d.ts.map +1 -0
  155. package/lib/rsi-service-environment.js +113 -0
  156. package/lib/rsi-service-environment.js.map +1 -0
  157. package/lib/rsi-service-setup.d.ts +10 -0
  158. package/lib/rsi-service-setup.d.ts.map +1 -0
  159. package/lib/rsi-service-setup.js +34 -0
  160. package/lib/rsi-service-setup.js.map +1 -0
  161. package/lib/rsi-setup.d.ts +35 -2
  162. package/lib/rsi-setup.d.ts.map +1 -1
  163. package/lib/rsi-setup.js +475 -32
  164. package/lib/rsi-setup.js.map +1 -1
  165. package/lib/rsi-source-maintenance.d.ts +25 -0
  166. package/lib/rsi-source-maintenance.d.ts.map +1 -0
  167. package/lib/rsi-source-maintenance.js +348 -0
  168. package/lib/rsi-source-maintenance.js.map +1 -0
  169. package/lib/rsi-source-update.d.ts +26 -0
  170. package/lib/rsi-source-update.d.ts.map +1 -0
  171. package/lib/rsi-source-update.js +276 -0
  172. package/lib/rsi-source-update.js.map +1 -0
  173. package/lib/rsi-source.d.ts +34 -0
  174. package/lib/rsi-source.d.ts.map +1 -0
  175. package/lib/rsi-source.js +487 -0
  176. package/lib/rsi-source.js.map +1 -0
  177. package/lib/rsi-systemd-bootstrap.d.ts +46 -0
  178. package/lib/rsi-systemd-bootstrap.d.ts.map +1 -0
  179. package/lib/rsi-systemd-bootstrap.js +271 -0
  180. package/lib/rsi-systemd-bootstrap.js.map +1 -0
  181. package/lib/sdk.d.ts.map +1 -1
  182. package/lib/sdk.js +26 -1
  183. package/lib/sdk.js.map +1 -1
  184. package/lib/setup-business.d.ts +49 -0
  185. package/lib/setup-business.d.ts.map +1 -0
  186. package/lib/setup-business.js +335 -0
  187. package/lib/setup-business.js.map +1 -0
  188. package/lib/setup-profile.d.ts +1 -1
  189. package/lib/setup-profile.d.ts.map +1 -1
  190. package/lib/setup-profile.js +25 -2
  191. package/lib/setup-profile.js.map +1 -1
  192. package/lib/setup.d.ts +12 -1
  193. package/lib/setup.d.ts.map +1 -1
  194. package/lib/setup.js +265 -48
  195. package/lib/setup.js.map +1 -1
  196. package/lib/systemd.d.ts +10 -0
  197. package/lib/systemd.d.ts.map +1 -1
  198. package/lib/systemd.js +23 -6
  199. package/lib/systemd.js.map +1 -1
  200. package/lib/types.d.ts +7 -0
  201. package/lib/types.d.ts.map +1 -1
  202. package/lib/types.js.map +1 -1
  203. package/lib/version.d.ts +1 -1
  204. package/lib/version.js +1 -1
  205. package/package.json +33 -20
package/lib/setup.js CHANGED
@@ -1,11 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
2
10
  import { createHash, randomBytes } from 'node:crypto';
3
11
  import { accessSync, constants, realpathSync } from 'node:fs';
4
12
  import { chmod, lstat, mkdir, open, readFile, readdir, rename, rm, unlink } from 'node:fs/promises';
5
13
  import { homedir } from 'node:os';
6
- import { basename, dirname, isAbsolute, join, normalize, win32 } from 'node:path';
14
+ import { createRequire } from 'node:module';
15
+ import { basename, delimiter, dirname, isAbsolute, join, normalize, win32 } from 'node:path';
7
16
  import { DatabaseSync } from 'node:sqlite';
8
- import { fileURLToPath } from 'node:url';
17
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
18
  import { createInterface } from 'node:readline/promises';
10
19
  import { spawnSync } from 'node:child_process';
11
20
  import { registerApp } from '@larksuiteoapi/node-sdk';
@@ -15,6 +24,9 @@ import { createOfficialLarkTransport } from './sdk.js';
15
24
  import { installDshResidentService } from './resident.js';
16
25
  import { prepareDshSystemdUserService } from './systemd.js';
17
26
  import { configureLarkProfilePatch, refreshLarkAgentPolicyPatch } from './setup-profile.js';
27
+ import { ensureLarkBusinessCli } from './business-cli-install.js';
28
+ import { setupLarkBusinessTools } from './setup-business.js';
29
+ import { reconcileLarkBusinessSkill } from './business-skill.js';
18
30
  import { assertRawManagedProfileIntegrity, materializeManagedProfileOverride, } from './setup-materialization.js';
19
31
  const setupKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
20
32
  const keychainLocatorPattern = /^[A-Za-z0-9][A-Za-z0-9/._-]{0,255}$/u;
@@ -33,13 +45,14 @@ export function parseLarkSetupArgs(argv) {
33
45
  tenant: 'personal',
34
46
  createApp: false,
35
47
  calendarReadonly: false,
48
+ businessTools: true,
36
49
  appName: 'DSH Personal Assistant',
37
50
  timeoutMs: 300_000,
38
51
  installServiceOnly: false,
39
52
  refreshAgentPolicy: false,
40
53
  manageService: true,
41
54
  linuxCredentialProvider: 'auto',
42
- agentTools: 'preserve',
55
+ agentTools: 'auto',
43
56
  help: false,
44
57
  };
45
58
  for (let index = 0; index < argv.length; index += 1) {
@@ -66,6 +79,8 @@ export function parseLarkSetupArgs(argv) {
66
79
  result.createApp = true;
67
80
  else if (option === '--calendar-readonly')
68
81
  result.calendarReadonly = true;
82
+ else if (option === '--no-business-tools')
83
+ result.businessTools = false;
69
84
  else if (option === '--install-service')
70
85
  result.installServiceOnly = true;
71
86
  else if (option === '--refresh-agent-policy')
@@ -75,11 +90,11 @@ export function parseLarkSetupArgs(argv) {
75
90
  else if (option === '--linux-credential-provider') {
76
91
  result.linuxCredentialProvider = argumentValue(argv, index++, option);
77
92
  }
78
- else if (option === '--allow-agent-tools' || option === '--disable-agent-tools') {
79
- if (result.agentTools !== 'preserve') {
80
- throw new Error('lark-channel setup: --allow-agent-tools and --disable-agent-tools are mutually exclusive');
93
+ else if (option === '--allow-agent-tools' || option === '--disable-agent-tools' || option === '--preserve-agent-tools') {
94
+ if (result.agentTools !== 'auto') {
95
+ throw new Error('lark-channel setup: --allow-agent-tools, --disable-agent-tools and --preserve-agent-tools are mutually exclusive');
81
96
  }
82
- result.agentTools = option === '--allow-agent-tools' ? 'enable' : 'disable';
97
+ result.agentTools = option === '--allow-agent-tools' ? 'enable' : option === '--disable-agent-tools' ? 'disable' : 'preserve';
83
98
  }
84
99
  else if (option === '--app-name')
85
100
  result.appName = argumentValue(argv, index++, option);
@@ -100,12 +115,12 @@ export function parseLarkSetupArgs(argv) {
100
115
  if (!result.help && result.refreshAgentPolicy) {
101
116
  const incompatible = [
102
117
  '--domain', '--tenant', '--app-id', '--create-app', '--install-service',
103
- '--no-service', '--linux-credential-provider', '--app-name', '--timeout-ms',
118
+ '--no-service', '--linux-credential-provider', '--app-name', '--timeout-ms', '--no-business-tools',
104
119
  ].find(option => argv.includes(option));
105
120
  if (incompatible !== undefined) {
106
121
  throw new Error(`lark-channel setup: --refresh-agent-policy cannot be combined with ${incompatible}`);
107
122
  }
108
- if (result.agentTools === 'preserve') {
123
+ if (result.agentTools !== 'enable' && result.agentTools !== 'disable') {
109
124
  throw new Error('lark-channel setup: --refresh-agent-policy requires --allow-agent-tools or --disable-agent-tools');
110
125
  }
111
126
  }
@@ -118,10 +133,13 @@ export function parseLarkSetupArgs(argv) {
118
133
  if (result.installServiceOnly && (result.createApp || result.appId !== undefined)) {
119
134
  throw new Error('lark-channel setup: --install-service cannot be combined with application setup options');
120
135
  }
136
+ if (result.installServiceOnly && !result.businessTools) {
137
+ throw new Error('lark-channel setup: --install-service cannot be combined with --no-business-tools');
138
+ }
121
139
  if (result.installServiceOnly && argv.includes('--linux-credential-provider')) {
122
140
  throw new Error('lark-channel setup: --install-service cannot be combined with --linux-credential-provider');
123
141
  }
124
- if (result.installServiceOnly && result.agentTools !== 'preserve') {
142
+ if (result.installServiceOnly && result.agentTools !== 'auto') {
125
143
  throw new Error('lark-channel setup: --install-service cannot be combined with agent-tools options');
126
144
  }
127
145
  const appNameHasControlCharacter = [...result.appName]
@@ -151,7 +169,7 @@ export function createLarkRegistrationOptions(input) {
151
169
  desc: '由 DeepSeek Harness 驱动的个人助理',
152
170
  },
153
171
  addons: {
154
- preset: false,
172
+ preset: input.businessTools !== false,
155
173
  scopes: { tenant: [
156
174
  'application:bot.basic_info:read',
157
175
  'im:message.group_at_msg:readonly',
@@ -289,10 +307,13 @@ Options:
289
307
  --install-service Only install/restart the profile's resident service
290
308
  --refresh-agent-policy Only refresh policy from the existing channel binding
291
309
  --no-service Configure Lark without installing a resident service
310
+ --no-business-tools Only configure the message channel; skip business CLI and user authorization
292
311
  --linux-credential-provider <mode>
293
312
  auto, protected-file, or secret-service (default: auto)
294
313
  --allow-agent-tools Allow mounted foreground/external Agent capabilities
295
314
  --disable-agent-tools Remove setup-managed Agent capability rules
315
+ --preserve-agent-tools Preserve existing rules, including on first setup
316
+ Default: allow for a new owner, preserve on reconfiguration
296
317
  --timeout-ms <ms> Owner DM wait, 30000..900000 (default: 300000)
297
318
  -h, --help Show this help
298
319
 
@@ -1146,6 +1167,7 @@ async function registerLarkApplication(args) {
1146
1167
  appName: args.appName,
1147
1168
  ...(args.appId === undefined ? {} : { appId: args.appId }),
1148
1169
  ...(args.calendarReadonly ? { calendarReadonly: true } : {}),
1170
+ businessTools: args.businessTools,
1149
1171
  signal: abortController.signal,
1150
1172
  onQRCodeReady(info) {
1151
1173
  process.stdout.write(`\n请在飞书中打开以下链接,选择已有应用或创建新应用(${info.expireIn} 秒内有效):\n${info.url}\n\n`);
@@ -1210,7 +1232,7 @@ export function dshHomeLifecycleRendezvousPath(dshHome, uid = process.getuid?.()
1210
1232
  if (!isAbsolute(dshHome))
1211
1233
  throw new Error('lark-channel setup: DSH_HOME must be absolute');
1212
1234
  if (!Number.isSafeInteger(uid) || uid === undefined || uid < 0) {
1213
- throw new Error('lark-channel setup: cannot determine the Linux user id for lifecycle locking');
1235
+ throw new Error('lark-channel setup: cannot determine the user id for lifecycle locking');
1214
1236
  }
1215
1237
  const canonicalHome = normalizeSetupResourcePath(dshHome);
1216
1238
  return `/tmp/.dsh-enhanced-lifecycle-${uid}-${textSha256(canonicalHome)}.lock`;
@@ -1226,13 +1248,15 @@ async function assertDshHomeLifecycleLockIdentity(descriptor, lockPath, uid) {
1226
1248
  }
1227
1249
  }
1228
1250
  export async function withDshHomeLifecycleLock(dshHome, operation) {
1229
- if (process.platform !== 'linux')
1251
+ if (process.platform !== 'linux' && process.platform !== 'darwin')
1230
1252
  return operation();
1231
1253
  const uid = process.getuid?.();
1232
1254
  if (!Number.isSafeInteger(uid) || uid === undefined || uid < 0) {
1233
- throw new Error('lark-channel setup: cannot determine the Linux user id for lifecycle locking');
1255
+ throw new Error('lark-channel setup: cannot determine the user id for lifecycle locking');
1234
1256
  }
1235
- const temporaryDirectory = await lstat('/tmp');
1257
+ // macOS /tmp is a system symlink to /private/tmp. Validate the physical
1258
+ // sticky directory while retaining the shared rendezvous pathname.
1259
+ const temporaryDirectory = await lstat(process.platform === 'darwin' ? realpathSync('/tmp') : '/tmp');
1236
1260
  if (!temporaryDirectory.isDirectory() || temporaryDirectory.isSymbolicLink()
1237
1261
  || temporaryDirectory.uid !== 0 || (temporaryDirectory.mode & 0o7777) !== 0o1777) {
1238
1262
  throw new Error('lark-channel setup: lifecycle rendezvous lock requires a safe root-owned sticky /tmp');
@@ -1242,9 +1266,16 @@ export async function withDshHomeLifecycleLock(dshHome, operation) {
1242
1266
  try {
1243
1267
  descriptor = await open(lockPath, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
1244
1268
  await assertDshHomeLifecycleLockIdentity(descriptor, lockPath, uid);
1245
- const acquired = spawnSync('/usr/bin/flock', ['-n', '-E', '75', '3'], {
1269
+ // Both helpers lock the inherited open file description. The kernel keeps
1270
+ // it locked until this process closes the descriptor, including on crash.
1271
+ const mac = process.platform === 'darwin';
1272
+ const acquired = spawnSync(mac ? '/usr/bin/perl' : '/usr/bin/flock', mac
1273
+ ? ['-MFcntl=:flock', '-MErrno=EAGAIN,EWOULDBLOCK', '-e',
1274
+ 'open(F, "<&", 3) or exit 74; flock(F, LOCK_EX|LOCK_NB) or exit(($! == EAGAIN || $! == EWOULDBLOCK) ? 75 : 74);']
1275
+ : ['-n', '-E', '75', '3'], {
1246
1276
  encoding: 'utf8',
1247
1277
  stdio: ['ignore', 'ignore', 'pipe', descriptor.fd],
1278
+ env: { PATH: '/usr/bin:/bin', LANG: 'C' }, timeout: 10_000,
1248
1279
  });
1249
1280
  if (acquired.status === 75)
1250
1281
  throw new Error(`lark-channel setup: DSH_HOME lifecycle is busy: ${lockPath}`);
@@ -1881,6 +1912,40 @@ function effectiveProfileHasEnabledLarkChannel(effectiveProfile, profile) {
1881
1912
  throw new Error(`lark-channel setup: profile ${profile} has an invalid Lark config`);
1882
1913
  return config.get('enabled') !== false;
1883
1914
  }
1915
+ function effectiveDeliveryConsumerDatabasePaths(effectiveProfile, dshHome, profile) {
1916
+ const document = parseDocument(effectiveProfile, { uniqueKeys: true });
1917
+ if (document.errors.length > 0 || !isSeq(document.contents)) {
1918
+ throw new Error(`lark-channel setup: cannot verify effective Delivery consumers in profile ${profile}`);
1919
+ }
1920
+ const paths = [];
1921
+ for (const row of document.contents.items) {
1922
+ if (!isMap(row))
1923
+ continue;
1924
+ const id = row.get('id');
1925
+ const name = row.get('name');
1926
+ if (id !== 'dsh-enhanced-assistant-delivery' && name !== '@dsh-enhanced/assistant-delivery')
1927
+ continue;
1928
+ if (!setupRowIsEnabled(row))
1929
+ continue;
1930
+ if (name !== '@dsh-enhanced/assistant-delivery') {
1931
+ throw new Error(`lark-channel setup: profile ${profile} has an invalid effective Delivery package row`);
1932
+ }
1933
+ const config = row.get('config', true);
1934
+ if (!isMap(config))
1935
+ throw new Error(`lark-channel setup: profile ${profile} has an invalid Delivery config`);
1936
+ const schedulerEnabled = config.get('schedulerEnabled');
1937
+ if (schedulerEnabled !== undefined && typeof schedulerEnabled !== 'boolean') {
1938
+ throw new Error(`lark-channel setup: profile ${profile} has a non-boolean Delivery schedulerEnabled value`);
1939
+ }
1940
+ // Delivery defaults to starting its scheduler independently of the Lark
1941
+ // adapter. A stopped service may restart later; configuration alone only
1942
+ // proves that it cannot consume when the scheduler is explicitly disabled.
1943
+ if (schedulerEnabled === false)
1944
+ continue;
1945
+ paths.push(dshEffectiveDataPath(config.get('databasePath'), dshHome, 'assistant-delivery databasePath'));
1946
+ }
1947
+ return paths;
1948
+ }
1884
1949
  function externalPrincipalKeyId(principal) {
1885
1950
  return [principal.channel, principal.account, principal.tenant, principal.user]
1886
1951
  .map(component => encodeURIComponent(component))
@@ -2181,7 +2246,7 @@ function assertEffectiveLarkOwnerBinding(input) {
2181
2246
  throw new Error(`lark-channel setup: effective Lark binding no longer represents the ${suffix}`);
2182
2247
  }
2183
2248
  }
2184
- async function assertNoOtherLarkProfileSharesDelivery(input) {
2249
+ async function assertNoOtherProfileSharesDelivery(input) {
2185
2250
  let entries;
2186
2251
  try {
2187
2252
  entries = await readdir(platformJoin(input.dshHome, 'profiles'), { withFileTypes: true });
@@ -2198,38 +2263,136 @@ async function assertNoOtherLarkProfileSharesDelivery(input) {
2198
2263
  || !setupKeyPattern.test(entry.name))
2199
2264
  continue;
2200
2265
  const patchPath = platformJoin(input.dshHome, 'profiles', entry.name, 'cordis.patch.yml');
2201
- // A profile can inherit Lark entirely from a lower bundle while its raw
2266
+ // A profile can inherit Delivery or Lark from a lower bundle while its raw
2202
2267
  // override contains no channel row. Activation and database identity must
2203
2268
  // therefore come from effective config whenever that profile composes.
2204
2269
  try {
2205
2270
  await readFile(patchPath, 'utf8');
2206
2271
  }
2207
2272
  catch (error) {
2208
- if (fileSystemErrorCode(error) === 'ENOENT')
2273
+ // Electron can have a bundle-backed desktop profile before its optional
2274
+ // user patch exists. Its bundles may still own Lark or consume Delivery.
2275
+ if (fileSystemErrorCode(error) === 'ENOENT' && entry.name !== 'desktop')
2209
2276
  continue;
2210
- throw error;
2277
+ if (fileSystemErrorCode(error) === 'ENOENT') {
2278
+ // Continue into the read-only Desktop composition below.
2279
+ }
2280
+ else {
2281
+ throw error;
2282
+ }
2211
2283
  }
2212
2284
  let effective;
2213
2285
  try {
2214
2286
  effective = await input.readEffectiveProfile(entry.name);
2215
2287
  }
2216
2288
  catch (error) {
2217
- // Raw absence cannot prove absence: Lark may be inherited from a lower
2218
- // bundle and a resident using the last valid composition may still run.
2219
- // Without a durable ownership registry, an unverifiable real profile is
2220
- // therefore a fail-closed handoff boundary.
2221
- throw new Error(`lark-channel setup: cannot verify effective Lark ownership in profile ${entry.name}`, {
2222
- cause: error,
2223
- });
2289
+ if (entry.name === 'desktop'
2290
+ && error instanceof Error
2291
+ && error.message.includes('profile "desktop" is managed exclusively by the Electron application')) {
2292
+ try {
2293
+ effective = await readElectronDesktopEffectiveProfile(input.dshHome);
2294
+ }
2295
+ catch (compositionError) {
2296
+ throw new Error(`lark-channel setup: cannot verify effective Lark ownership in profile desktop or its Delivery consumers`, {
2297
+ cause: compositionError,
2298
+ });
2299
+ }
2300
+ }
2301
+ else {
2302
+ // Raw absence cannot prove absence: consumers may be inherited from a lower
2303
+ // bundle and a resident using the last valid composition may still run.
2304
+ // Without a durable ownership registry, an unverifiable real profile is
2305
+ // therefore a fail-closed handoff boundary.
2306
+ throw new Error(`lark-channel setup: cannot verify effective Lark ownership in profile ${entry.name} or its Delivery consumers`, {
2307
+ cause: error,
2308
+ });
2309
+ }
2224
2310
  }
2225
- if (!effectiveProfileHasEnabledLarkChannel(effective, entry.name))
2226
- continue;
2227
- const otherDatabasePath = deliveryDatabasePathFromEffectiveProfile(effective, input.dshHome);
2228
- if (normalizeSetupResourcePath(otherDatabasePath) === databasePath) {
2311
+ if (effectiveProfileHasEnabledLarkChannel(effective, entry.name)
2312
+ && normalizeSetupResourcePath(deliveryDatabasePathFromEffectiveProfile(effective, input.dshHome)) === databasePath) {
2229
2313
  throw new Error(`lark-channel setup: profile ${entry.name} already owns Lark for the same assistant-delivery database; `
2230
2314
  + 'refusing to revoke its owner from another profile');
2231
2315
  }
2316
+ if (effectiveDeliveryConsumerDatabasePaths(effective, input.dshHome, entry.name)
2317
+ .some(path => normalizeSetupResourcePath(path) === databasePath)) {
2318
+ throw new Error(`lark-channel setup: profile ${entry.name} has an enabled assistant-delivery scheduler for the same database; `
2319
+ + 'disable its scheduler or configure a separate database before continuing');
2320
+ }
2321
+ }
2322
+ }
2323
+ /** Electron owns `desktop`, so the DSH CLI refuses even a config dump. Compose
2324
+ * its existing project with the same app-boot reader as Electron, without
2325
+ * running the CLI or preparing (and writing to) that profile. */
2326
+ async function readElectronDesktopEffectiveProfile(dshHome) {
2327
+ const profileDir = platformJoin(dshHome, 'profiles', 'desktop');
2328
+ // The Electron Host resolves core bundles from its private runtime, not the
2329
+ // plugin project. The setup process already uses `dsh` on PATH for ordinary
2330
+ // config dumps, so anchor this exceptional read to that same installation.
2331
+ const dshManifest = await resolveSetupDshManifest();
2332
+ const bootEntry = createRequire(dshManifest).resolve('@deepseek-ai/dsh-app-boot');
2333
+ const boot = await import(__rewriteRelativeImportExtension(pathToFileURL(bootEntry).href));
2334
+ const loaded = boot.loadProfileDirectory('dsh desktop', profileDir, dshManifest);
2335
+ if (loaded.skippedBundles.length > 0) {
2336
+ throw new Error(`desktop profile has unreadable bundles: ${loaded.skippedBundles.map(item => item.packageName).join(', ')}`);
2337
+ }
2338
+ const layers = loaded.layers.map(layer => ({ label: layer.packageName, patches: layer.patches }));
2339
+ layers.push({ label: loaded.patchPath, patches: loaded.patches });
2340
+ const homePatchPath = platformJoin(dshHome, 'cordis.patch.yml');
2341
+ const homePatches = boot.loadOptionalPatches('dsh desktop', homePatchPath);
2342
+ if (homePatches !== undefined)
2343
+ layers.push({ label: homePatchPath, patches: homePatches });
2344
+ return boot.renderConfigDump('dsh desktop', platformJoin(profileDir, 'cordis.yml'), layers);
2345
+ }
2346
+ async function resolveSetupDshManifest() {
2347
+ const selectedVersion = spawnSync('dsh', ['--version'], {
2348
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
2349
+ });
2350
+ if (selectedVersion.status !== 0 || typeof selectedVersion.stdout !== 'string') {
2351
+ throw new Error('cannot verify the selected dsh CLI version');
2352
+ }
2353
+ const version = selectedVersion.stdout.trim();
2354
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version)) {
2355
+ throw new Error('selected dsh CLI returned an invalid version');
2232
2356
  }
2357
+ for (const directory of (process.env.PATH ?? '').split(delimiter)) {
2358
+ if (directory === '')
2359
+ continue;
2360
+ const executable = platformJoin(directory, 'dsh');
2361
+ try {
2362
+ accessSync(executable, constants.X_OK);
2363
+ }
2364
+ catch {
2365
+ continue;
2366
+ }
2367
+ const canonical = realpathSync(executable);
2368
+ const candidates = [];
2369
+ try {
2370
+ candidates.push(createRequire(canonical).resolve('@deepseek-ai/dsh/package.json'));
2371
+ }
2372
+ catch {
2373
+ // Electron's public `dsh` command is a shell launcher; it has no Node
2374
+ // package at this path. Search later PATH entries for a matching npm CLI.
2375
+ }
2376
+ for (let ancestor = dirname(canonical);;) {
2377
+ candidates.push(platformJoin(ancestor, 'package.json'));
2378
+ const parent = dirname(ancestor);
2379
+ if (parent === ancestor)
2380
+ break;
2381
+ ancestor = parent;
2382
+ }
2383
+ for (const manifest of candidates) {
2384
+ let packageJson;
2385
+ try {
2386
+ packageJson = JSON.parse(await readFile(manifest, 'utf8'));
2387
+ }
2388
+ catch {
2389
+ continue;
2390
+ }
2391
+ if (packageJson.name === '@deepseek-ai/dsh' && packageJson.version === version)
2392
+ return manifest;
2393
+ }
2394
+ }
2395
+ throw new Error('no npm dsh installation on PATH matches the selected CLI version');
2233
2396
  }
2234
2397
  async function withDeliveryOwnerSetupLock(databasePath, operation, options = {}) {
2235
2398
  const resourcePath = normalizeSetupResourcePath(databasePath);
@@ -2406,7 +2569,7 @@ async function recoverLarkSetupJournalUnlocked(input) {
2406
2569
  stagedCredential: journal.stagedCredential,
2407
2570
  context: 'journal',
2408
2571
  });
2409
- await assertNoOtherLarkProfileSharesDelivery({
2572
+ await assertNoOtherProfileSharesDelivery({
2410
2573
  dshHome: input.dshHome,
2411
2574
  profile: input.profile,
2412
2575
  databasePath,
@@ -2588,7 +2751,7 @@ export async function executeLarkSetupProfileTransaction(input) {
2588
2751
  effectiveProfile,
2589
2752
  });
2590
2753
  await withDeliveryOwnerSetupLock(databasePath, async () => {
2591
- await assertNoOtherLarkProfileSharesDelivery({
2754
+ await assertNoOtherProfileSharesDelivery({
2592
2755
  dshHome: input.dshHome,
2593
2756
  profile: input.args.profile,
2594
2757
  databasePath,
@@ -2824,7 +2987,7 @@ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
2824
2987
  const effectiveProfile = await readEffectiveProfile(args.profile);
2825
2988
  const databasePath = deliveryDatabasePathFromEffectiveProfile(effectiveProfile, dshHome);
2826
2989
  await withDeliveryOwnerSetupLock(databasePath, async () => {
2827
- await assertNoOtherLarkProfileSharesDelivery({
2990
+ await assertNoOtherProfileSharesDelivery({
2828
2991
  dshHome,
2829
2992
  profile: args.profile,
2830
2993
  databasePath,
@@ -2911,7 +3074,7 @@ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
2911
3074
  });
2912
3075
  const databasePath = deliveryDatabasePathFromEffectiveProfile(await readEffectiveProfile(args.profile), dshHome);
2913
3076
  return withDeliveryOwnerSetupLock(databasePath, async () => {
2914
- await assertNoOtherLarkProfileSharesDelivery({
3077
+ await assertNoOtherProfileSharesDelivery({
2915
3078
  dshHome,
2916
3079
  profile: args.profile,
2917
3080
  databasePath,
@@ -2971,7 +3134,7 @@ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
2971
3134
  });
2972
3135
  const databasePath = deliveryDatabasePathFromEffectiveProfile(await readEffectiveProfile(args.profile), dshHome);
2973
3136
  await withDeliveryOwnerSetupLock(databasePath, async () => {
2974
- await assertNoOtherLarkProfileSharesDelivery({
3137
+ await assertNoOtherProfileSharesDelivery({
2975
3138
  dshHome,
2976
3139
  profile: args.profile,
2977
3140
  databasePath,
@@ -2979,6 +3142,9 @@ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
2979
3142
  });
2980
3143
  }, runtime.profileLockOptions);
2981
3144
  }, runtime.profileLockOptions);
3145
+ const businessCli = args.businessTools
3146
+ ? await (runtime.ensureBusinessCli ?? ensureLarkBusinessCli)({ dshHome })
3147
+ : undefined;
2982
3148
  let appId = args.appId;
2983
3149
  let createApp = args.createApp;
2984
3150
  if (appId === undefined && !createApp) {
@@ -2995,9 +3161,9 @@ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
2995
3161
  let generatedSecret;
2996
3162
  if (createApp) {
2997
3163
  process.stdout.write(args.appId === undefined
2998
- ? '\n将通过飞书官方设备授权选择已有应用或创建新应用,并增量申请最小权限。App Secret 不会显示或写入 profile。\n'
2999
- : `\n将通过飞书官方设备授权更新已有应用 ${args.appId},并增量申请最小权限。App Secret 不会显示或写入 profile。\n`);
3000
- const registration = await registerLarkApplication(args);
3164
+ ? '\n将通过飞书官方设备授权选择已有应用或创建新应用,并增量申请应用权限。App Secret 不会显示或写入 profile。\n'
3165
+ : `\n将通过飞书官方设备授权更新已有应用 ${args.appId},并增量申请应用权限。App Secret 不会显示或写入 profile。\n`);
3166
+ const registration = await (runtime.registerApplication ?? registerLarkApplication)(args);
3001
3167
  appId = registration.client_id;
3002
3168
  resolvedDomain = registration.user_info?.tenant_brand ?? resolvedDomain;
3003
3169
  generatedSecret = registration.client_secret;
@@ -3014,6 +3180,7 @@ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
3014
3180
  throw new Error('lark-channel setup: Feishu did not return an App ID');
3015
3181
  const configuredAppId = appId;
3016
3182
  let installedService;
3183
+ let businessBinding;
3017
3184
  await executeLarkSetupProfileTransaction({
3018
3185
  args,
3019
3186
  dshHome,
@@ -3049,17 +3216,38 @@ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
3049
3216
  : undefined);
3050
3217
  },
3051
3218
  readEffectiveProfile,
3219
+ validateProfile: validate,
3220
+ ...runtime.ownerSetupOperations,
3052
3221
  async discoverOwner(ownerInput) {
3053
- const phrase = `DSH-CONNECT-${randomBytes(4).toString('hex').toUpperCase()}`;
3054
- const transport = createOfficialLarkTransport({
3055
- appId: ownerInput.appId,
3056
- appSecret: ownerInput.appSecret,
3057
- domain: ownerInput.domain,
3058
- handshakeTimeoutMs: 15_000,
3059
- requestTimeoutMs: 30_000,
3060
- imageDownloadTimeoutMs: 30_000,
3222
+ let owner;
3223
+ if (runtime.discoverOwner !== undefined)
3224
+ owner = await runtime.discoverOwner(ownerInput);
3225
+ else {
3226
+ const phrase = `DSH-CONNECT-${randomBytes(4).toString('hex').toUpperCase()}`;
3227
+ const transport = createOfficialLarkTransport({
3228
+ appId: ownerInput.appId,
3229
+ appSecret: ownerInput.appSecret,
3230
+ domain: ownerInput.domain,
3231
+ handshakeTimeoutMs: 15_000,
3232
+ requestTimeoutMs: 30_000,
3233
+ imageDownloadTimeoutMs: 30_000,
3234
+ });
3235
+ owner = await discoverOwner(transport, phrase, ownerInput.account, ownerInput.tenant, ownerInput.timeoutMs);
3236
+ }
3237
+ if (args.businessTools)
3238
+ businessBinding = { appSecret: ownerInput.appSecret, owner };
3239
+ const previousDocument = parseDocument(await readEffectiveProfile(args.profile), { uniqueKeys: true });
3240
+ const previousRow = isSeq(previousDocument.contents)
3241
+ ? previousDocument.contents.items.find(item => isMap(item) && item.get('id') === 'dsh-enhanced-lark-channel')
3242
+ : undefined;
3243
+ const previousConfig = isMap(previousRow) ? previousRow.get('config', true) : undefined;
3244
+ const previousAccount = isMap(previousConfig) ? previousConfig.get('account') : undefined;
3245
+ await reconcileLarkBusinessSkill({
3246
+ dshHome, profile: args.profile, account: args.account, appId: ownerInput.appId,
3247
+ ownerUserId: owner.user, enabled: args.businessTools,
3248
+ ...(typeof previousAccount === 'string' ? { previousAccount } : {}),
3061
3249
  });
3062
- return discoverOwner(transport, phrase, ownerInput.account, ownerInput.tenant, ownerInput.timeoutMs);
3250
+ return owner;
3063
3251
  },
3064
3252
  ...(args.manageService ? {
3065
3253
  async afterCommit() {
@@ -3068,6 +3256,35 @@ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
3068
3256
  } : {}),
3069
3257
  },
3070
3258
  });
3259
+ if (businessCli !== undefined) {
3260
+ const binding = businessBinding;
3261
+ if (binding === undefined)
3262
+ throw new Error('lark-channel setup: business tools require the completed owner binding');
3263
+ process.stdout.write('\n消息通道已配置;正在接入飞书业务工具并检查 owner 用户授权。\n');
3264
+ const business = await (runtime.setupBusinessTools ?? setupLarkBusinessTools)({
3265
+ dshHome, profile: args.profile, account: args.account, appId: configuredAppId,
3266
+ appSecret: binding.appSecret, domain: resolvedDomain, ownerUserId: binding.owner.user,
3267
+ cli: businessCli, timeoutMs: args.timeoutMs,
3268
+ async onAuthorization(url) {
3269
+ process.stdout.write(`\n请用刚才绑定的飞书账号完成业务权限授权:\n${url}\n\n`);
3270
+ try {
3271
+ process.stdout.write(`${await QRCode.toString(url, { type: 'terminal', small: true })}\n`);
3272
+ }
3273
+ catch {
3274
+ process.stdout.write('终端二维码生成失败,请打开上面的链接。\n');
3275
+ }
3276
+ },
3277
+ });
3278
+ businessBinding = undefined;
3279
+ process.stdout.write(`飞书业务工具已接入${business.reusedAuthorization ? ',复用已有 owner 授权' : ''}。技能:${business.skillPath}\n`);
3280
+ if (business.permissionCompletion === 'partial') {
3281
+ process.stdout.write(`用户登录已完成;本次申请仍有 ${business.missingScopes.length} 项权限未获批,业务工具仅使用实际已授予的权限。\n`
3282
+ + `未获批权限:${business.missingScopes.join(', ')}\n`);
3283
+ }
3284
+ else if (business.permissionCompletion === 'unknown') {
3285
+ process.stdout.write('已验证 owner 登录;当前授权证据不足以确认完整权限清单,具体操作仍以平台当前授权为准。\n');
3286
+ }
3287
+ }
3071
3288
  if (args.manageService) {
3072
3289
  const service = installedService;
3073
3290
  if (service === undefined)