@dsh-enhanced/lark-channel 0.1.23 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/docs/operations.md +8 -0
- package/docs/setup.md +3 -0
- package/lib/adapter.d.ts +7 -0
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +18 -4
- package/lib/adapter.js.map +1 -1
- package/lib/config.d.ts +3 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +8 -0
- package/lib/config.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/sdk.d.ts +17 -2
- package/lib/sdk.d.ts.map +1 -1
- package/lib/sdk.js +170 -76
- package/lib/sdk.js.map +1 -1
- package/lib/service.d.ts +17 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +68 -5
- package/lib/service.js.map +1 -1
- package/lib/setup-profile.d.ts.map +1 -1
- package/lib/setup-profile.js +1 -0
- package/lib/setup-profile.js.map +1 -1
- package/lib/setup.d.ts +4 -0
- package/lib/setup.d.ts.map +1 -1
- package/lib/setup.js +313 -242
- package/lib/setup.js.map +1 -1
- package/lib/supervised-growth-profile.d.ts +27 -0
- package/lib/supervised-growth-profile.d.ts.map +1 -1
- package/lib/supervised-growth-profile.js +112 -0
- package/lib/supervised-growth-profile.js.map +1 -1
- package/lib/supervised-growth-setup.d.ts +82 -1
- package/lib/supervised-growth-setup.d.ts.map +1 -1
- package/lib/supervised-growth-setup.js +430 -85
- package/lib/supervised-growth-setup.js.map +1 -1
- package/lib/types.d.ts +12 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +14 -14
package/lib/setup.js
CHANGED
|
@@ -32,6 +32,7 @@ export function parseLarkSetupArgs(argv) {
|
|
|
32
32
|
accountProvided: false,
|
|
33
33
|
tenant: 'personal',
|
|
34
34
|
createApp: false,
|
|
35
|
+
calendarReadonly: false,
|
|
35
36
|
appName: 'DSH Personal Assistant',
|
|
36
37
|
timeoutMs: 300_000,
|
|
37
38
|
installServiceOnly: false,
|
|
@@ -63,6 +64,8 @@ export function parseLarkSetupArgs(argv) {
|
|
|
63
64
|
result.appId = argumentValue(argv, index++, option);
|
|
64
65
|
else if (option === '--create-app')
|
|
65
66
|
result.createApp = true;
|
|
67
|
+
else if (option === '--calendar-readonly')
|
|
68
|
+
result.calendarReadonly = true;
|
|
66
69
|
else if (option === '--install-service')
|
|
67
70
|
result.installServiceOnly = true;
|
|
68
71
|
else if (option === '--refresh-agent-policy')
|
|
@@ -109,6 +112,9 @@ export function parseLarkSetupArgs(argv) {
|
|
|
109
112
|
if (result.installServiceOnly && !result.manageService) {
|
|
110
113
|
throw new Error('lark-channel setup: --install-service and --no-service cannot be used together');
|
|
111
114
|
}
|
|
115
|
+
if (result.calendarReadonly && !result.createApp) {
|
|
116
|
+
throw new Error('lark-channel setup: --calendar-readonly requires --create-app so the application scope can be updated');
|
|
117
|
+
}
|
|
112
118
|
if (result.installServiceOnly && (result.createApp || result.appId !== undefined)) {
|
|
113
119
|
throw new Error('lark-channel setup: --install-service cannot be combined with application setup options');
|
|
114
120
|
}
|
|
@@ -153,6 +159,7 @@ export function createLarkRegistrationOptions(input) {
|
|
|
153
159
|
'im:message.reactions:write_only',
|
|
154
160
|
'im:message:send_as_bot',
|
|
155
161
|
'im:resource',
|
|
162
|
+
...(input.calendarReadonly ? ['calendar:calendar:readonly'] : []),
|
|
156
163
|
] },
|
|
157
164
|
events: { items: { tenant: ['im.message.receive_v1'] } },
|
|
158
165
|
callbacks: { items: ['card.action.trigger'] },
|
|
@@ -276,6 +283,7 @@ Options:
|
|
|
276
283
|
--account <id> Local channel account id (default: primary)
|
|
277
284
|
--tenant <id> Local tenant namespace (default: personal)
|
|
278
285
|
--create-app Select, create, or officially update an app
|
|
286
|
+
--calendar-readonly With --create-app, add Calendar read-only permission for exact calendar triggers
|
|
279
287
|
--app-name <name> New app name (default: DSH Personal Assistant)
|
|
280
288
|
--app-id <cli_...> Existing app id; combine with --create-app to update it
|
|
281
289
|
--install-service Only install/restart the profile's resident service
|
|
@@ -1137,6 +1145,7 @@ async function registerLarkApplication(args) {
|
|
|
1137
1145
|
domain: args.domain,
|
|
1138
1146
|
appName: args.appName,
|
|
1139
1147
|
...(args.appId === undefined ? {} : { appId: args.appId }),
|
|
1148
|
+
...(args.calendarReadonly ? { calendarReadonly: true } : {}),
|
|
1140
1149
|
signal: abortController.signal,
|
|
1141
1150
|
onQRCodeReady(info) {
|
|
1142
1151
|
process.stdout.write(`\n请在飞书中打开以下链接,选择已有应用或创建新应用(${info.expireIn} 秒内有效):\n${info.url}\n\n`);
|
|
@@ -1197,6 +1206,65 @@ async function discoverOwner(transport, phrase, account, tenant, timeoutMs) {
|
|
|
1197
1206
|
await transport.disconnect().catch(() => { });
|
|
1198
1207
|
}
|
|
1199
1208
|
}
|
|
1209
|
+
export function dshHomeLifecycleRendezvousPath(dshHome, uid = process.getuid?.()) {
|
|
1210
|
+
if (!isAbsolute(dshHome))
|
|
1211
|
+
throw new Error('lark-channel setup: DSH_HOME must be absolute');
|
|
1212
|
+
if (!Number.isSafeInteger(uid) || uid === undefined || uid < 0) {
|
|
1213
|
+
throw new Error('lark-channel setup: cannot determine the Linux user id for lifecycle locking');
|
|
1214
|
+
}
|
|
1215
|
+
const canonicalHome = normalizeSetupResourcePath(dshHome);
|
|
1216
|
+
return `/tmp/.dsh-enhanced-lifecycle-${uid}-${textSha256(canonicalHome)}.lock`;
|
|
1217
|
+
}
|
|
1218
|
+
async function assertDshHomeLifecycleLockIdentity(descriptor, lockPath, uid) {
|
|
1219
|
+
const [opened, linked] = await Promise.all([descriptor.stat(), lstat(lockPath)]);
|
|
1220
|
+
if (!opened.isFile() || !linked.isFile() || linked.isSymbolicLink()
|
|
1221
|
+
|| opened.uid !== uid || linked.uid !== uid
|
|
1222
|
+
|| opened.nlink !== 1 || linked.nlink !== 1
|
|
1223
|
+
|| (opened.mode & 0o777) !== 0o600 || (linked.mode & 0o777) !== 0o600
|
|
1224
|
+
|| opened.dev !== linked.dev || opened.ino !== linked.ino) {
|
|
1225
|
+
throw new Error(`lark-channel setup: lifecycle rendezvous lock is unsafe: ${lockPath}`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
export async function withDshHomeLifecycleLock(dshHome, operation) {
|
|
1229
|
+
if (process.platform !== 'linux')
|
|
1230
|
+
return operation();
|
|
1231
|
+
const uid = process.getuid?.();
|
|
1232
|
+
if (!Number.isSafeInteger(uid) || uid === undefined || uid < 0) {
|
|
1233
|
+
throw new Error('lark-channel setup: cannot determine the Linux user id for lifecycle locking');
|
|
1234
|
+
}
|
|
1235
|
+
const temporaryDirectory = await lstat('/tmp');
|
|
1236
|
+
if (!temporaryDirectory.isDirectory() || temporaryDirectory.isSymbolicLink()
|
|
1237
|
+
|| temporaryDirectory.uid !== 0 || (temporaryDirectory.mode & 0o7777) !== 0o1777) {
|
|
1238
|
+
throw new Error('lark-channel setup: lifecycle rendezvous lock requires a safe root-owned sticky /tmp');
|
|
1239
|
+
}
|
|
1240
|
+
const lockPath = dshHomeLifecycleRendezvousPath(dshHome, uid);
|
|
1241
|
+
let descriptor;
|
|
1242
|
+
try {
|
|
1243
|
+
descriptor = await open(lockPath, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
1244
|
+
await assertDshHomeLifecycleLockIdentity(descriptor, lockPath, uid);
|
|
1245
|
+
const acquired = spawnSync('/usr/bin/flock', ['-n', '-E', '75', '3'], {
|
|
1246
|
+
encoding: 'utf8',
|
|
1247
|
+
stdio: ['ignore', 'ignore', 'pipe', descriptor.fd],
|
|
1248
|
+
});
|
|
1249
|
+
if (acquired.status === 75)
|
|
1250
|
+
throw new Error(`lark-channel setup: DSH_HOME lifecycle is busy: ${lockPath}`);
|
|
1251
|
+
if (acquired.status !== 0) {
|
|
1252
|
+
const detail = typeof acquired.stderr === 'string' ? acquired.stderr.trim().slice(0, 1_000) : '';
|
|
1253
|
+
throw new Error(`lark-channel setup: cannot acquire DSH_HOME lifecycle lock${detail === '' ? '' : `: ${detail}`}`);
|
|
1254
|
+
}
|
|
1255
|
+
await assertDshHomeLifecycleLockIdentity(descriptor, lockPath, uid);
|
|
1256
|
+
return await operation();
|
|
1257
|
+
}
|
|
1258
|
+
catch (error) {
|
|
1259
|
+
if (fileSystemErrorCode(error) === 'ELOOP') {
|
|
1260
|
+
throw new Error(`lark-channel setup: lifecycle rendezvous lock is unsafe: ${lockPath}`);
|
|
1261
|
+
}
|
|
1262
|
+
throw error;
|
|
1263
|
+
}
|
|
1264
|
+
finally {
|
|
1265
|
+
await descriptor?.close();
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1200
1268
|
const defaultProfileSetupLockOptions = {
|
|
1201
1269
|
timeoutMs: 30_000,
|
|
1202
1270
|
pollMs: 100,
|
|
@@ -2733,285 +2801,288 @@ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
|
|
|
2733
2801
|
const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
|
|
2734
2802
|
if (!isAbsolute(dshHome))
|
|
2735
2803
|
throw new Error('lark-channel setup: DSH_HOME must be absolute');
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
await
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
profile: args.profile,
|
|
2753
|
-
operations: recoveryOperations,
|
|
2754
|
-
});
|
|
2755
|
-
const effectiveProfile = await readEffectiveProfile(args.profile);
|
|
2756
|
-
const databasePath = deliveryDatabasePathFromEffectiveProfile(effectiveProfile, dshHome);
|
|
2757
|
-
await withDeliveryOwnerSetupLock(databasePath, async () => {
|
|
2758
|
-
await assertNoOtherLarkProfileSharesDelivery({
|
|
2804
|
+
return withDshHomeLifecycleLock(dshHome, async () => {
|
|
2805
|
+
const patchPath = join(dshHome, 'profiles', args.profile, 'cordis.patch.yml');
|
|
2806
|
+
const validate = runtime.validateProfile ?? validateProfile;
|
|
2807
|
+
const readEffectiveProfile = runtime.readEffectiveProfile ?? dumpProfile;
|
|
2808
|
+
const installResidentService = runtime.installResidentService ?? installDshResidentService;
|
|
2809
|
+
const prepareResidentService = runtime.prepareResidentService ?? prepareDshSystemdUserService;
|
|
2810
|
+
const recoveryOperations = {
|
|
2811
|
+
...runtime.journalOperations,
|
|
2812
|
+
readEffectiveProfile: runtime.journalOperations?.readEffectiveProfile ?? readEffectiveProfile,
|
|
2813
|
+
installService: runtime.journalOperations?.installService
|
|
2814
|
+
?? (async () => { await installResidentService({ dshHome, profile: args.profile }); }),
|
|
2815
|
+
};
|
|
2816
|
+
if (args.refreshAgentPolicy) {
|
|
2817
|
+
await withProfileSetupLock(patchPath, async () => {
|
|
2818
|
+
await recoverLarkSetupJournalUnlocked({
|
|
2819
|
+
patchPath,
|
|
2759
2820
|
dshHome,
|
|
2760
2821
|
profile: args.profile,
|
|
2761
|
-
|
|
2762
|
-
readEffectiveProfile,
|
|
2763
|
-
});
|
|
2764
|
-
const originalPatch = await readFile(patchPath, 'utf8');
|
|
2765
|
-
const materializedPatch = materializeManagedProfileOverride({
|
|
2766
|
-
profilePatch: originalPatch,
|
|
2767
|
-
effectiveProfile,
|
|
2822
|
+
operations: recoveryOperations,
|
|
2768
2823
|
});
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2824
|
+
const effectiveProfile = await readEffectiveProfile(args.profile);
|
|
2825
|
+
const databasePath = deliveryDatabasePathFromEffectiveProfile(effectiveProfile, dshHome);
|
|
2826
|
+
await withDeliveryOwnerSetupLock(databasePath, async () => {
|
|
2827
|
+
await assertNoOtherLarkProfileSharesDelivery({
|
|
2828
|
+
dshHome,
|
|
2829
|
+
profile: args.profile,
|
|
2830
|
+
databasePath,
|
|
2831
|
+
readEffectiveProfile,
|
|
2832
|
+
});
|
|
2833
|
+
const originalPatch = await readFile(patchPath, 'utf8');
|
|
2834
|
+
const materializedPatch = materializeManagedProfileOverride({
|
|
2835
|
+
profilePatch: originalPatch,
|
|
2836
|
+
effectiveProfile,
|
|
2837
|
+
});
|
|
2838
|
+
const refreshInput = {
|
|
2839
|
+
profilePatch: materializedPatch,
|
|
2840
|
+
effectiveProfile,
|
|
2841
|
+
dshHome,
|
|
2842
|
+
agentTools: args.agentTools === 'enable' ? 'enable' : 'disable',
|
|
2843
|
+
...(args.accountProvided ? { account: args.account } : {}),
|
|
2844
|
+
};
|
|
2845
|
+
const updatedPatch = refreshLarkAgentPolicyFromEffective(refreshInput);
|
|
2846
|
+
const journal = {
|
|
2847
|
+
patchPath,
|
|
2848
|
+
dshHome,
|
|
2849
|
+
profile: args.profile,
|
|
2850
|
+
operation: 'refresh',
|
|
2851
|
+
phase: 'candidate',
|
|
2852
|
+
originalPatch,
|
|
2853
|
+
updatedPatch,
|
|
2854
|
+
installService: false,
|
|
2855
|
+
transactionId: randomBytes(16).toString('hex'),
|
|
2856
|
+
};
|
|
2857
|
+
await persistLarkSetupJournal(journal);
|
|
2858
|
+
try {
|
|
2859
|
+
await atomicWrite(patchPath, updatedPatch);
|
|
2860
|
+
await validate(args.profile);
|
|
2861
|
+
const validatedEffectiveProfile = await readEffectiveProfile(args.profile);
|
|
2862
|
+
const validatedDatabasePath = deliveryDatabasePathFromEffectiveProfile(validatedEffectiveProfile, dshHome);
|
|
2863
|
+
if (normalizeSetupResourcePath(validatedDatabasePath) !== normalizeSetupResourcePath(databasePath)) {
|
|
2864
|
+
throw new Error('lark-channel setup: effective assistant-delivery database changed during refresh validation');
|
|
2865
|
+
}
|
|
2866
|
+
const validatedSemanticPatch = refreshLarkAgentPolicyFromEffective({
|
|
2867
|
+
...refreshInput,
|
|
2868
|
+
effectiveProfile: validatedEffectiveProfile,
|
|
2869
|
+
});
|
|
2870
|
+
assertEffectiveManagedPolicyMatchesCandidate({
|
|
2871
|
+
candidatePatch: validatedSemanticPatch,
|
|
2872
|
+
effectiveProfile: validatedEffectiveProfile,
|
|
2873
|
+
});
|
|
2874
|
+
assertEffectiveManagedPolicyMatchesCandidate({
|
|
2875
|
+
candidatePatch: updatedPatch,
|
|
2876
|
+
effectiveProfile: validatedEffectiveProfile,
|
|
2877
|
+
});
|
|
2878
|
+
if (await readFile(patchPath, 'utf8') !== updatedPatch) {
|
|
2879
|
+
throw new Error('lark-channel setup: refreshed profile changed during validation');
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
catch (error) {
|
|
2883
|
+
if (await rollbackCandidatePatch({ patchPath, originalPatch, updatedPatch }) === 'conflict') {
|
|
2884
|
+
throw new Error('lark-channel setup: rollback refused because the profile changed concurrently', { cause: error });
|
|
2885
|
+
}
|
|
2886
|
+
await clearLarkSetupJournal(patchPath);
|
|
2887
|
+
throw error;
|
|
2888
|
+
}
|
|
2889
|
+
// If this durable phase transition or journal removal reports an error,
|
|
2890
|
+
// leave recovery to inspect the on-disk phase. Rolling back here could
|
|
2891
|
+
// contradict a validated record already renamed into place.
|
|
2892
|
+
await persistLarkSetupJournal({ ...journal, phase: 'validated' });
|
|
2893
|
+
await clearLarkSetupJournal(patchPath);
|
|
2894
|
+
}, runtime.profileLockOptions);
|
|
2895
|
+
}, runtime.profileLockOptions);
|
|
2896
|
+
process.stdout.write(`DSH profile ${args.profile} 的 Agent capability policy 已刷新;飞书应用、凭据和 owner 绑定未改动。\n`);
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
if (args.installServiceOnly) {
|
|
2900
|
+
if (process.platform === 'linux') {
|
|
2901
|
+
const prepared = await prepareResidentService({ dshHome, profile: args.profile });
|
|
2902
|
+
if (prepared?.enabledLinger)
|
|
2903
|
+
process.stdout.write('已为当前 Linux 用户启用 linger,注销 SSH 后服务仍会保持。\n');
|
|
2904
|
+
}
|
|
2905
|
+
const service = await withProfileSetupLock(patchPath, async () => {
|
|
2906
|
+
await recoverLarkSetupJournalUnlocked({
|
|
2778
2907
|
patchPath,
|
|
2779
2908
|
dshHome,
|
|
2780
2909
|
profile: args.profile,
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2910
|
+
operations: recoveryOperations,
|
|
2911
|
+
});
|
|
2912
|
+
const databasePath = deliveryDatabasePathFromEffectiveProfile(await readEffectiveProfile(args.profile), dshHome);
|
|
2913
|
+
return withDeliveryOwnerSetupLock(databasePath, async () => {
|
|
2914
|
+
await assertNoOtherLarkProfileSharesDelivery({
|
|
2915
|
+
dshHome,
|
|
2916
|
+
profile: args.profile,
|
|
2917
|
+
databasePath,
|
|
2918
|
+
readEffectiveProfile,
|
|
2919
|
+
});
|
|
2920
|
+
const profileSnapshot = await readFile(patchPath, 'utf8');
|
|
2791
2921
|
await validate(args.profile);
|
|
2792
|
-
const
|
|
2793
|
-
const validatedDatabasePath = deliveryDatabasePathFromEffectiveProfile(validatedEffectiveProfile, dshHome);
|
|
2922
|
+
const validatedDatabasePath = deliveryDatabasePathFromEffectiveProfile(await readEffectiveProfile(args.profile), dshHome);
|
|
2794
2923
|
if (normalizeSetupResourcePath(validatedDatabasePath) !== normalizeSetupResourcePath(databasePath)) {
|
|
2795
|
-
throw new Error('lark-channel setup: effective assistant-delivery database changed during
|
|
2796
|
-
}
|
|
2797
|
-
const validatedSemanticPatch = refreshLarkAgentPolicyFromEffective({
|
|
2798
|
-
...refreshInput,
|
|
2799
|
-
effectiveProfile: validatedEffectiveProfile,
|
|
2800
|
-
});
|
|
2801
|
-
assertEffectiveManagedPolicyMatchesCandidate({
|
|
2802
|
-
candidatePatch: validatedSemanticPatch,
|
|
2803
|
-
effectiveProfile: validatedEffectiveProfile,
|
|
2804
|
-
});
|
|
2805
|
-
assertEffectiveManagedPolicyMatchesCandidate({
|
|
2806
|
-
candidatePatch: updatedPatch,
|
|
2807
|
-
effectiveProfile: validatedEffectiveProfile,
|
|
2808
|
-
});
|
|
2809
|
-
if (await readFile(patchPath, 'utf8') !== updatedPatch) {
|
|
2810
|
-
throw new Error('lark-channel setup: refreshed profile changed during validation');
|
|
2924
|
+
throw new Error('lark-channel setup: effective assistant-delivery database changed during service validation');
|
|
2811
2925
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
if (await rollbackCandidatePatch({ patchPath, originalPatch, updatedPatch }) === 'conflict') {
|
|
2815
|
-
throw new Error('lark-channel setup: rollback refused because the profile changed concurrently', { cause: error });
|
|
2926
|
+
if (await readFile(patchPath, 'utf8') !== profileSnapshot) {
|
|
2927
|
+
throw new Error('lark-channel setup: profile changed during resident service validation');
|
|
2816
2928
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
}
|
|
2820
|
-
// If this durable phase transition or journal removal reports an error,
|
|
2821
|
-
// leave recovery to inspect the on-disk phase. Rolling back here could
|
|
2822
|
-
// contradict a validated record already renamed into place.
|
|
2823
|
-
await persistLarkSetupJournal({ ...journal, phase: 'validated' });
|
|
2824
|
-
await clearLarkSetupJournal(patchPath);
|
|
2929
|
+
return installResidentService({ dshHome, profile: args.profile });
|
|
2930
|
+
}, runtime.profileLockOptions);
|
|
2825
2931
|
}, runtime.profileLockOptions);
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
if (process.platform === 'linux') {
|
|
2932
|
+
process.stdout.write(`DSH profile ${args.profile} 已由 ${service.kind} 启动并保持常驻。\n`
|
|
2933
|
+
+ `状态:${service.statusCommand}\n日志:${service.logCommand}\n`);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
const preferredCredentialProvider = credentialProviderForPlatform(process.platform);
|
|
2937
|
+
if (process.platform === 'linux' && args.manageService) {
|
|
2832
2938
|
const prepared = await prepareResidentService({ dshHome, profile: args.profile });
|
|
2833
2939
|
if (prepared?.enabledLinger)
|
|
2834
2940
|
process.stdout.write('已为当前 Linux 用户启用 linger,注销 SSH 后服务仍会保持。\n');
|
|
2835
2941
|
}
|
|
2836
|
-
|
|
2942
|
+
let credentialProvider;
|
|
2943
|
+
if (runtime.preflightCredentialProvider === undefined) {
|
|
2944
|
+
const selection = await selectLarkCredentialProvider(preferredCredentialProvider, {
|
|
2945
|
+
dshHome,
|
|
2946
|
+
mode: args.linuxCredentialProvider,
|
|
2947
|
+
});
|
|
2948
|
+
credentialProvider = selection.provider;
|
|
2949
|
+
if (selection.fallbackReason !== undefined) {
|
|
2950
|
+
process.stdout.write('Linux Secret Service 当前不可用;向导已自动切换到当前用户 0600 私有凭据文件。'
|
|
2951
|
+
+ '该文件未额外加密,同一 UID 与 root 可读取,但不会进入 profile、命令参数、环境变量或日志。\n');
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
else {
|
|
2955
|
+
if (args.linuxCredentialProvider !== 'auto') {
|
|
2956
|
+
throw new Error('lark-channel setup: an injected credential preflight cannot be combined with an explicit Linux provider');
|
|
2957
|
+
}
|
|
2958
|
+
credentialProvider = await runtime.preflightCredentialProvider(preferredCredentialProvider)
|
|
2959
|
+
?? preferredCredentialProvider;
|
|
2960
|
+
}
|
|
2961
|
+
// Resolve a prior crash before prompting the user or mutating a cloud app.
|
|
2962
|
+
// The mutation transaction repeats this check after registration to close
|
|
2963
|
+
// the gap against a different setup process that completed in the meantime.
|
|
2964
|
+
await withProfileSetupLock(patchPath, async () => {
|
|
2837
2965
|
await recoverLarkSetupJournalUnlocked({
|
|
2838
2966
|
patchPath,
|
|
2839
2967
|
dshHome,
|
|
2840
2968
|
profile: args.profile,
|
|
2841
2969
|
operations: recoveryOperations,
|
|
2970
|
+
allowDeferredSecretServiceCleanup: credentialProvider === 'linux-protected-file',
|
|
2842
2971
|
});
|
|
2843
2972
|
const databasePath = deliveryDatabasePathFromEffectiveProfile(await readEffectiveProfile(args.profile), dshHome);
|
|
2844
|
-
|
|
2973
|
+
await withDeliveryOwnerSetupLock(databasePath, async () => {
|
|
2845
2974
|
await assertNoOtherLarkProfileSharesDelivery({
|
|
2846
2975
|
dshHome,
|
|
2847
2976
|
profile: args.profile,
|
|
2848
2977
|
databasePath,
|
|
2849
2978
|
readEffectiveProfile,
|
|
2850
2979
|
});
|
|
2851
|
-
const profileSnapshot = await readFile(patchPath, 'utf8');
|
|
2852
|
-
await validate(args.profile);
|
|
2853
|
-
const validatedDatabasePath = deliveryDatabasePathFromEffectiveProfile(await readEffectiveProfile(args.profile), dshHome);
|
|
2854
|
-
if (normalizeSetupResourcePath(validatedDatabasePath) !== normalizeSetupResourcePath(databasePath)) {
|
|
2855
|
-
throw new Error('lark-channel setup: effective assistant-delivery database changed during service validation');
|
|
2856
|
-
}
|
|
2857
|
-
if (await readFile(patchPath, 'utf8') !== profileSnapshot) {
|
|
2858
|
-
throw new Error('lark-channel setup: profile changed during resident service validation');
|
|
2859
|
-
}
|
|
2860
|
-
return installResidentService({ dshHome, profile: args.profile });
|
|
2861
2980
|
}, runtime.profileLockOptions);
|
|
2862
2981
|
}, runtime.profileLockOptions);
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
if (runtime.preflightCredentialProvider === undefined) {
|
|
2875
|
-
const selection = await selectLarkCredentialProvider(preferredCredentialProvider, {
|
|
2876
|
-
dshHome,
|
|
2877
|
-
mode: args.linuxCredentialProvider,
|
|
2878
|
-
});
|
|
2879
|
-
credentialProvider = selection.provider;
|
|
2880
|
-
if (selection.fallbackReason !== undefined) {
|
|
2881
|
-
process.stdout.write('Linux Secret Service 当前不可用;向导已自动切换到当前用户 0600 私有凭据文件。'
|
|
2882
|
-
+ '该文件未额外加密,同一 UID 与 root 可读取,但不会进入 profile、命令参数、环境变量或日志。\n');
|
|
2982
|
+
let appId = args.appId;
|
|
2983
|
+
let createApp = args.createApp;
|
|
2984
|
+
if (appId === undefined && !createApp) {
|
|
2985
|
+
const prompts = createInterface({ input: process.stdin, output: process.stdout });
|
|
2986
|
+
try {
|
|
2987
|
+
appId = (await prompts.question('已有飞书 App ID(cli_...;直接回车则打开一键选择/创建):')).trim();
|
|
2988
|
+
createApp = appId.length === 0;
|
|
2989
|
+
}
|
|
2990
|
+
finally {
|
|
2991
|
+
prompts.close();
|
|
2992
|
+
}
|
|
2883
2993
|
}
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
if (
|
|
2887
|
-
|
|
2994
|
+
let resolvedDomain = args.domain;
|
|
2995
|
+
let generatedSecret;
|
|
2996
|
+
if (createApp) {
|
|
2997
|
+
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);
|
|
3001
|
+
appId = registration.client_id;
|
|
3002
|
+
resolvedDomain = registration.user_info?.tenant_brand ?? resolvedDomain;
|
|
3003
|
+
generatedSecret = registration.client_secret;
|
|
3004
|
+
process.stdout.write(`应用 ${appId} 已完成授权;凭据将在 profile 事务中暂存并激活。\n`);
|
|
2888
3005
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
3006
|
+
else {
|
|
3007
|
+
if (appId === undefined || !/^cli_[0-9a-fA-F]{16}$/u.test(appId)) {
|
|
3008
|
+
throw new Error('lark-channel setup: invalid App ID');
|
|
3009
|
+
}
|
|
3010
|
+
process.stdout.write(`请确认已有应用已开启机器人、im:message 与 im:resource 权限、长连接事件 im.message.receive_v1,且版本已发布。\n`
|
|
3011
|
+
+ `控制台:https://open.feishu.cn/app\n`);
|
|
3012
|
+
}
|
|
3013
|
+
if (appId === undefined)
|
|
3014
|
+
throw new Error('lark-channel setup: Feishu did not return an App ID');
|
|
3015
|
+
const configuredAppId = appId;
|
|
3016
|
+
let installedService;
|
|
3017
|
+
await executeLarkSetupProfileTransaction({
|
|
3018
|
+
args,
|
|
2898
3019
|
dshHome,
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3020
|
+
patchPath,
|
|
3021
|
+
application: { appId: configuredAppId, domain: resolvedDomain },
|
|
3022
|
+
credentialProvider,
|
|
3023
|
+
...(runtime.profileLockOptions === undefined ? {} : { profileLockOptions: runtime.profileLockOptions }),
|
|
3024
|
+
operations: {
|
|
3025
|
+
async storeCredential(locator) {
|
|
3026
|
+
const service = locator.provider === 'windows-dpapi'
|
|
3027
|
+
? `dsh/lark/${args.profile}/${args.account}/versions/windows-dpapi`
|
|
3028
|
+
: locator.provider === 'linux-protected-file'
|
|
3029
|
+
? `dsh/lark/${args.profile}/${args.account}/versions/linux-protected-file`
|
|
3030
|
+
: locator.service;
|
|
3031
|
+
const credentialPath = locator.provider === 'windows-dpapi'
|
|
3032
|
+
|| locator.provider === 'linux-protected-file' ? locator.path : undefined;
|
|
3033
|
+
if (generatedSecret === undefined) {
|
|
3034
|
+
await storeSecret(credentialProvider, service, args.account, credentialPath);
|
|
3035
|
+
}
|
|
3036
|
+
else {
|
|
3037
|
+
await storeGeneratedSecret(credentialProvider, service, args.account, generatedSecret, credentialPath);
|
|
3038
|
+
}
|
|
3039
|
+
process.stdout.write(`候选凭据已暂存到 ${credentialProvider};旧凭据仍保持有效。\n`);
|
|
3040
|
+
},
|
|
3041
|
+
readCredential(locator) {
|
|
3042
|
+
const service = locator.provider === 'windows-dpapi'
|
|
3043
|
+
? `dsh/lark/${args.profile}/${args.account}/versions/windows-dpapi`
|
|
3044
|
+
: locator.provider === 'linux-protected-file'
|
|
3045
|
+
? `dsh/lark/${args.profile}/${args.account}/versions/linux-protected-file`
|
|
3046
|
+
: locator.service;
|
|
3047
|
+
return readSecret(credentialProvider, service, args.account, locator.provider === 'windows-dpapi' || locator.provider === 'linux-protected-file'
|
|
3048
|
+
? locator.path
|
|
3049
|
+
: undefined);
|
|
3050
|
+
},
|
|
2909
3051
|
readEffectiveProfile,
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
3052
|
+
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,
|
|
3061
|
+
});
|
|
3062
|
+
return discoverOwner(transport, phrase, ownerInput.account, ownerInput.tenant, ownerInput.timeoutMs);
|
|
3063
|
+
},
|
|
3064
|
+
...(args.manageService ? {
|
|
3065
|
+
async afterCommit() {
|
|
3066
|
+
installedService = await installResidentService({ dshHome, profile: args.profile });
|
|
3067
|
+
},
|
|
3068
|
+
} : {}),
|
|
3069
|
+
},
|
|
3070
|
+
});
|
|
3071
|
+
if (args.manageService) {
|
|
3072
|
+
const service = installedService;
|
|
3073
|
+
if (service === undefined)
|
|
3074
|
+
throw new Error('lark-channel setup: resident service installation did not complete');
|
|
3075
|
+
process.stdout.write(`\n飞书 channel 已写入 profile ${args.profile},owner 已精确绑定。\n`
|
|
3076
|
+
+ `DSH Host 已由 ${service.kind} 启动并保持常驻,无需再手动运行 dsh web。\n`
|
|
3077
|
+
+ `状态:${service.statusCommand}\n日志:${service.logCommand}\n`
|
|
3078
|
+
+ `健康检查:在 Web 会话中调用 assistant_health。\n`);
|
|
2920
3079
|
}
|
|
2921
|
-
|
|
2922
|
-
|
|
3080
|
+
else {
|
|
3081
|
+
process.stdout.write(`\n飞书 channel 已写入 profile ${args.profile},owner 已精确绑定。\n`
|
|
3082
|
+
+ `本次跳过常驻服务;手动启动:dsh --profile ${args.profile} --no-open\n`
|
|
3083
|
+
+ `健康检查:在 Web 会话中调用 assistant_health。\n`);
|
|
2923
3084
|
}
|
|
2924
|
-
}
|
|
2925
|
-
let resolvedDomain = args.domain;
|
|
2926
|
-
let generatedSecret;
|
|
2927
|
-
if (createApp) {
|
|
2928
|
-
process.stdout.write(args.appId === undefined
|
|
2929
|
-
? '\n将通过飞书官方设备授权选择已有应用或创建新应用,并增量申请最小权限。App Secret 不会显示或写入 profile。\n'
|
|
2930
|
-
: `\n将通过飞书官方设备授权更新已有应用 ${args.appId},并增量申请最小权限。App Secret 不会显示或写入 profile。\n`);
|
|
2931
|
-
const registration = await registerLarkApplication(args);
|
|
2932
|
-
appId = registration.client_id;
|
|
2933
|
-
resolvedDomain = registration.user_info?.tenant_brand ?? resolvedDomain;
|
|
2934
|
-
generatedSecret = registration.client_secret;
|
|
2935
|
-
process.stdout.write(`应用 ${appId} 已完成授权;凭据将在 profile 事务中暂存并激活。\n`);
|
|
2936
|
-
}
|
|
2937
|
-
else {
|
|
2938
|
-
if (appId === undefined || !/^cli_[0-9a-fA-F]{16}$/u.test(appId)) {
|
|
2939
|
-
throw new Error('lark-channel setup: invalid App ID');
|
|
2940
|
-
}
|
|
2941
|
-
process.stdout.write(`请确认已有应用已开启机器人、im:message 与 im:resource 权限、长连接事件 im.message.receive_v1,且版本已发布。\n`
|
|
2942
|
-
+ `控制台:https://open.feishu.cn/app\n`);
|
|
2943
|
-
}
|
|
2944
|
-
if (appId === undefined)
|
|
2945
|
-
throw new Error('lark-channel setup: Feishu did not return an App ID');
|
|
2946
|
-
const configuredAppId = appId;
|
|
2947
|
-
let installedService;
|
|
2948
|
-
await executeLarkSetupProfileTransaction({
|
|
2949
|
-
args,
|
|
2950
|
-
dshHome,
|
|
2951
|
-
patchPath,
|
|
2952
|
-
application: { appId: configuredAppId, domain: resolvedDomain },
|
|
2953
|
-
credentialProvider,
|
|
2954
|
-
...(runtime.profileLockOptions === undefined ? {} : { profileLockOptions: runtime.profileLockOptions }),
|
|
2955
|
-
operations: {
|
|
2956
|
-
async storeCredential(locator) {
|
|
2957
|
-
const service = locator.provider === 'windows-dpapi'
|
|
2958
|
-
? `dsh/lark/${args.profile}/${args.account}/versions/windows-dpapi`
|
|
2959
|
-
: locator.provider === 'linux-protected-file'
|
|
2960
|
-
? `dsh/lark/${args.profile}/${args.account}/versions/linux-protected-file`
|
|
2961
|
-
: locator.service;
|
|
2962
|
-
const credentialPath = locator.provider === 'windows-dpapi'
|
|
2963
|
-
|| locator.provider === 'linux-protected-file' ? locator.path : undefined;
|
|
2964
|
-
if (generatedSecret === undefined) {
|
|
2965
|
-
await storeSecret(credentialProvider, service, args.account, credentialPath);
|
|
2966
|
-
}
|
|
2967
|
-
else {
|
|
2968
|
-
await storeGeneratedSecret(credentialProvider, service, args.account, generatedSecret, credentialPath);
|
|
2969
|
-
}
|
|
2970
|
-
process.stdout.write(`候选凭据已暂存到 ${credentialProvider};旧凭据仍保持有效。\n`);
|
|
2971
|
-
},
|
|
2972
|
-
readCredential(locator) {
|
|
2973
|
-
const service = locator.provider === 'windows-dpapi'
|
|
2974
|
-
? `dsh/lark/${args.profile}/${args.account}/versions/windows-dpapi`
|
|
2975
|
-
: locator.provider === 'linux-protected-file'
|
|
2976
|
-
? `dsh/lark/${args.profile}/${args.account}/versions/linux-protected-file`
|
|
2977
|
-
: locator.service;
|
|
2978
|
-
return readSecret(credentialProvider, service, args.account, locator.provider === 'windows-dpapi' || locator.provider === 'linux-protected-file'
|
|
2979
|
-
? locator.path
|
|
2980
|
-
: undefined);
|
|
2981
|
-
},
|
|
2982
|
-
readEffectiveProfile,
|
|
2983
|
-
async discoverOwner(ownerInput) {
|
|
2984
|
-
const phrase = `DSH-CONNECT-${randomBytes(4).toString('hex').toUpperCase()}`;
|
|
2985
|
-
const transport = createOfficialLarkTransport({
|
|
2986
|
-
appId: ownerInput.appId,
|
|
2987
|
-
appSecret: ownerInput.appSecret,
|
|
2988
|
-
domain: ownerInput.domain,
|
|
2989
|
-
handshakeTimeoutMs: 15_000,
|
|
2990
|
-
imageDownloadTimeoutMs: 30_000,
|
|
2991
|
-
});
|
|
2992
|
-
return discoverOwner(transport, phrase, ownerInput.account, ownerInput.tenant, ownerInput.timeoutMs);
|
|
2993
|
-
},
|
|
2994
|
-
...(args.manageService ? {
|
|
2995
|
-
async afterCommit() {
|
|
2996
|
-
installedService = await installResidentService({ dshHome, profile: args.profile });
|
|
2997
|
-
},
|
|
2998
|
-
} : {}),
|
|
2999
|
-
},
|
|
3000
3085
|
});
|
|
3001
|
-
if (args.manageService) {
|
|
3002
|
-
const service = installedService;
|
|
3003
|
-
if (service === undefined)
|
|
3004
|
-
throw new Error('lark-channel setup: resident service installation did not complete');
|
|
3005
|
-
process.stdout.write(`\n飞书 channel 已写入 profile ${args.profile},owner 已精确绑定。\n`
|
|
3006
|
-
+ `DSH Host 已由 ${service.kind} 启动并保持常驻,无需再手动运行 dsh web。\n`
|
|
3007
|
-
+ `状态:${service.statusCommand}\n日志:${service.logCommand}\n`
|
|
3008
|
-
+ `健康检查:在 Web 会话中调用 assistant_health。\n`);
|
|
3009
|
-
}
|
|
3010
|
-
else {
|
|
3011
|
-
process.stdout.write(`\n飞书 channel 已写入 profile ${args.profile},owner 已精确绑定。\n`
|
|
3012
|
-
+ `本次跳过常驻服务;手动启动:dsh --profile ${args.profile} --no-open\n`
|
|
3013
|
-
+ `健康检查:在 Web 会话中调用 assistant_health。\n`);
|
|
3014
|
-
}
|
|
3015
3086
|
}
|
|
3016
3087
|
if (process.argv[1] !== undefined && isMainEntry(import.meta.url, process.argv[1])) {
|
|
3017
3088
|
void runLarkSetup().catch(error => {
|