@nextclaw/kernel 0.6.28 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +108 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +718 -304
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { catchError, filter, from, lastValueFrom, tap } from "rxjs";
|
|
|
10
10
|
import { appendFileSync, chmodSync, constants, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import path, { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
12
12
|
import { access, appendFile, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
13
|
-
import { AppHomeService, AppInstallationService, AppManifestService, AppRegistryService, isAppComponentManifestBundle } from "@nextclaw/app-runtime";
|
|
13
|
+
import { AppHomeService, AppInstallationService, AppInstanceInventoryService, AppInstanceStorageService, AppManifestService, AppRegistryService, FileLockService, isAppComponentManifestBundle } from "@nextclaw/app-runtime";
|
|
14
14
|
import { execFileSync, spawn } from "node:child_process";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { BUILTIN_PROVIDER_PLUGINS } from "@nextclaw/runtime";
|
|
@@ -2686,6 +2686,52 @@ var AppPackageOperationManager = class {
|
|
|
2686
2686
|
isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2687
2687
|
};
|
|
2688
2688
|
//#endregion
|
|
2689
|
+
//#region src/services/app-package-presentation.service.ts
|
|
2690
|
+
var AppPackagePresentationService = class {
|
|
2691
|
+
readManifest = async (manifestPath) => {
|
|
2692
|
+
const candidate = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
2693
|
+
const rawIcon = typeof candidate.icon === "string" ? candidate.icon : void 0;
|
|
2694
|
+
return {
|
|
2695
|
+
...typeof candidate.title === "string" ? { title: candidate.title } : {},
|
|
2696
|
+
...typeof candidate.description === "string" ? { description: candidate.description } : {},
|
|
2697
|
+
...rawIcon ? { icon: await this.resolveIcon(manifestPath, rawIcon) } : {},
|
|
2698
|
+
...this.readLocalizedField(candidate, "nameI18n"),
|
|
2699
|
+
...this.readLocalizedField(candidate, "titleI18n"),
|
|
2700
|
+
...this.readLocalizedField(candidate, "descriptionI18n")
|
|
2701
|
+
};
|
|
2702
|
+
};
|
|
2703
|
+
readLocalizedField = (candidate, field) => {
|
|
2704
|
+
const value = candidate[field];
|
|
2705
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2706
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
2707
|
+
return entries.length > 0 ? { [field]: Object.fromEntries(entries) } : {};
|
|
2708
|
+
};
|
|
2709
|
+
resolveIcon = async (manifestPath, icon) => {
|
|
2710
|
+
if (this.isDirectIconReference(icon)) return icon;
|
|
2711
|
+
const manifestDirectory = path.dirname(manifestPath);
|
|
2712
|
+
const iconPath = path.resolve(manifestDirectory, icon);
|
|
2713
|
+
const relative = path.relative(manifestDirectory, iconPath);
|
|
2714
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) return icon;
|
|
2715
|
+
const mimeType = this.iconMimeType(path.extname(iconPath));
|
|
2716
|
+
if (!mimeType) return icon;
|
|
2717
|
+
const bytes = await readFile(iconPath);
|
|
2718
|
+
if (bytes.byteLength > 256 * 1024) return icon;
|
|
2719
|
+
return `data:${mimeType};base64,${bytes.toString("base64")}`;
|
|
2720
|
+
};
|
|
2721
|
+
isDirectIconReference = (icon) => icon.startsWith("data:") || icon.startsWith("http://") || icon.startsWith("https://") || icon.startsWith("/") || !icon.includes("/") && !icon.includes(".") && [...icon].length <= 8;
|
|
2722
|
+
iconMimeType = (extension) => {
|
|
2723
|
+
switch (extension.toLowerCase()) {
|
|
2724
|
+
case ".svg": return "image/svg+xml";
|
|
2725
|
+
case ".png": return "image/png";
|
|
2726
|
+
case ".jpg":
|
|
2727
|
+
case ".jpeg": return "image/jpeg";
|
|
2728
|
+
case ".webp": return "image/webp";
|
|
2729
|
+
case ".gif": return "image/gif";
|
|
2730
|
+
default: return;
|
|
2731
|
+
}
|
|
2732
|
+
};
|
|
2733
|
+
};
|
|
2734
|
+
//#endregion
|
|
2689
2735
|
//#region src/types/app-package.types.ts
|
|
2690
2736
|
var AppPackageError = class extends Error {
|
|
2691
2737
|
constructor(code, message) {
|
|
@@ -2812,6 +2858,7 @@ var AppPackageManager = class {
|
|
|
2812
2858
|
installationService;
|
|
2813
2859
|
manifestService = new AppManifestService();
|
|
2814
2860
|
operationManager;
|
|
2861
|
+
presentationService = new AppPackagePresentationService();
|
|
2815
2862
|
registryService;
|
|
2816
2863
|
runtimeHooks = EMPTY_RUNTIME_HOOKS;
|
|
2817
2864
|
builtInBootstrapPromise;
|
|
@@ -2834,6 +2881,10 @@ var AppPackageManager = class {
|
|
|
2834
2881
|
const records = await this.registryService.listApps();
|
|
2835
2882
|
return { entries: await Promise.all(records.map(async (record) => await this.toPackageView(await this.installationService.info(record.appId)))) };
|
|
2836
2883
|
};
|
|
2884
|
+
listInstalledDataOwners = async () => (await this.registryService.listApps()).map((record) => ({
|
|
2885
|
+
id: record.appId,
|
|
2886
|
+
name: record.name
|
|
2887
|
+
}));
|
|
2837
2888
|
getPackage = async (appId) => {
|
|
2838
2889
|
await this.ensureBuiltInPackages();
|
|
2839
2890
|
try {
|
|
@@ -2845,7 +2896,9 @@ var AppPackageManager = class {
|
|
|
2845
2896
|
};
|
|
2846
2897
|
listActiveComponentSources = async () => {
|
|
2847
2898
|
await this.ensureBuiltInPackages();
|
|
2848
|
-
|
|
2899
|
+
const records = await this.registryService.listApps();
|
|
2900
|
+
return (await Promise.all(records.filter((record) => record.enabled).map(async (record) => {
|
|
2901
|
+
await this.installationService.assertVersionIntegrity(record.appId, record.activeVersion);
|
|
2849
2902
|
const version = record.installedVersions[record.activeVersion];
|
|
2850
2903
|
if (!version || version.manifestSchemaVersion !== 2) return [];
|
|
2851
2904
|
return (version.components ?? []).map((component) => ({
|
|
@@ -2855,9 +2908,12 @@ var AppPackageManager = class {
|
|
|
2855
2908
|
packageVersion: record.activeVersion,
|
|
2856
2909
|
sourcePath: component.componentDirectory,
|
|
2857
2910
|
manifestPath: component.manifestPath,
|
|
2858
|
-
dataDirectory: record.dataDirectory
|
|
2911
|
+
dataDirectory: record.dataDirectory,
|
|
2912
|
+
instanceId: record.defaultInstance.id,
|
|
2913
|
+
storage: record.defaultInstance.storage,
|
|
2914
|
+
...this.resolveSecurity(version, record.appId)
|
|
2859
2915
|
}));
|
|
2860
|
-
});
|
|
2916
|
+
}))).flat();
|
|
2861
2917
|
};
|
|
2862
2918
|
listOperations = async () => await this.operationManager.list();
|
|
2863
2919
|
startOperation = async (input) => {
|
|
@@ -2873,66 +2929,115 @@ var AppPackageManager = class {
|
|
|
2873
2929
|
return await this.getPackage(result.appId);
|
|
2874
2930
|
};
|
|
2875
2931
|
enable = async (appId) => {
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2932
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
2933
|
+
const app = await this.getPackage(appId);
|
|
2934
|
+
await this.installationService.assertVersionIntegrity(appId, app.activeVersion);
|
|
2935
|
+
if (app.enabled) return app;
|
|
2936
|
+
await this.assertEngineCompatibility(appId);
|
|
2937
|
+
const sources = this.toComponentSources(app);
|
|
2938
|
+
await this.runtimeHooks.assertCanActivate(sources);
|
|
2939
|
+
await this.installationService.setEnabled(appId, true);
|
|
2940
|
+
return await this.getPackage(appId);
|
|
2941
|
+
});
|
|
2883
2942
|
};
|
|
2884
2943
|
disable = async (appId) => {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2944
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
2945
|
+
const app = await this.getPackage(appId);
|
|
2946
|
+
if (!app.enabled) return app;
|
|
2947
|
+
await this.runtimeHooks.beforeDeactivate(this.toComponentSources(app));
|
|
2948
|
+
await this.installationService.setEnabled(appId, false);
|
|
2949
|
+
return await this.getPackage(appId);
|
|
2950
|
+
});
|
|
2890
2951
|
};
|
|
2891
2952
|
update = async (appId, options = {}) => {
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
if (updated
|
|
2899
|
-
|
|
2900
|
-
package: updated,
|
|
2953
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
2954
|
+
const current = await this.getPackage(appId);
|
|
2955
|
+
const result = await this.installationService.update(appId, {
|
|
2956
|
+
...options,
|
|
2957
|
+
activate: false
|
|
2958
|
+
});
|
|
2959
|
+
if (!result.updated) return {
|
|
2960
|
+
package: current,
|
|
2901
2961
|
result
|
|
2902
2962
|
};
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2963
|
+
let activated = false;
|
|
2964
|
+
let deactivated = false;
|
|
2965
|
+
try {
|
|
2966
|
+
await this.assertEngineCompatibility(appId, result.version);
|
|
2967
|
+
const candidate = await this.toPackageView(await this.installationService.info(appId), result.version);
|
|
2968
|
+
if (current.enabled) {
|
|
2969
|
+
await this.runtimeHooks.beforeDeactivate(this.toComponentSources(current));
|
|
2970
|
+
deactivated = true;
|
|
2971
|
+
await this.runtimeHooks.assertCanActivate(this.toComponentSources(candidate));
|
|
2972
|
+
}
|
|
2973
|
+
activated = (await this.installationService.rollback(appId, result.version)).rolledBack;
|
|
2974
|
+
return {
|
|
2975
|
+
package: await this.getPackage(appId),
|
|
2976
|
+
result
|
|
2977
|
+
};
|
|
2978
|
+
} catch (error) {
|
|
2979
|
+
if (activated) await this.installationService.rollback(appId, result.previousVersion);
|
|
2980
|
+
if (deactivated) try {
|
|
2981
|
+
await this.runtimeHooks.assertCanActivate(this.toComponentSources(current));
|
|
2982
|
+
} catch (recoveryError) {
|
|
2983
|
+
throw new AggregateError([error, recoveryError], `应用 ${appId} 更新失败,且旧 runtime 恢复探测失败。`);
|
|
2984
|
+
}
|
|
2985
|
+
throw error;
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2907
2988
|
};
|
|
2908
2989
|
rollback = async (appId, version) => {
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2990
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
2991
|
+
const current = await this.getPackage(appId);
|
|
2992
|
+
if (current.activeVersion === version) return {
|
|
2993
|
+
package: current,
|
|
2994
|
+
result: {
|
|
2995
|
+
appId,
|
|
2996
|
+
activeVersion: version,
|
|
2997
|
+
previousVersion: version,
|
|
2998
|
+
enabled: current.enabled,
|
|
2999
|
+
rolledBack: false
|
|
3000
|
+
}
|
|
2919
3001
|
};
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
3002
|
+
await this.assertEngineCompatibility(appId, version);
|
|
3003
|
+
const candidate = await this.toPackageView(await this.installationService.info(appId), version);
|
|
3004
|
+
let deactivated = false;
|
|
3005
|
+
let result;
|
|
3006
|
+
try {
|
|
3007
|
+
if (current.enabled) {
|
|
3008
|
+
await this.runtimeHooks.beforeDeactivate(this.toComponentSources(current));
|
|
3009
|
+
deactivated = true;
|
|
3010
|
+
await this.runtimeHooks.assertCanActivate(this.toComponentSources(candidate));
|
|
3011
|
+
}
|
|
3012
|
+
result = await this.installationService.rollback(appId, version);
|
|
3013
|
+
return {
|
|
3014
|
+
package: await this.getPackage(appId),
|
|
3015
|
+
result
|
|
3016
|
+
};
|
|
3017
|
+
} catch (error) {
|
|
3018
|
+
if (result?.rolledBack) await this.installationService.rollback(appId, result.previousVersion);
|
|
3019
|
+
if (deactivated) try {
|
|
3020
|
+
await this.runtimeHooks.assertCanActivate(this.toComponentSources(current));
|
|
3021
|
+
} catch (recoveryError) {
|
|
3022
|
+
throw new AggregateError([error, recoveryError], `应用 ${appId} 回滚失败,且旧 runtime 恢复探测失败。`);
|
|
3023
|
+
}
|
|
3024
|
+
throw error;
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
2924
3027
|
};
|
|
2925
3028
|
uninstall = async (appId, purgeData) => {
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3029
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
3030
|
+
const current = await this.getPackage(appId);
|
|
3031
|
+
if (current.builtIn) await this.registryService.setBuiltInSuppressed(appId, true);
|
|
3032
|
+
try {
|
|
3033
|
+
if (current.enabled) await this.runtimeHooks.beforeDeactivate(this.toComponentSources(current));
|
|
3034
|
+
await this.runtimeHooks.beforeUninstall(this.toComponentSources(current));
|
|
3035
|
+
return await this.installationService.uninstall(appId, purgeData);
|
|
3036
|
+
} catch (error) {
|
|
3037
|
+
if (current.builtIn) await this.registryService.setBuiltInSuppressed(appId, false);
|
|
3038
|
+
throw error;
|
|
3039
|
+
}
|
|
3040
|
+
});
|
|
2936
3041
|
};
|
|
2937
3042
|
ensureBuiltInPackages = async () => {
|
|
2938
3043
|
this.builtInBootstrapPromise ??= this.installBuiltInPackages();
|
|
@@ -2944,13 +3049,22 @@ var AppPackageManager = class {
|
|
|
2944
3049
|
if (!isAppComponentManifestBundle(manifest)) continue;
|
|
2945
3050
|
if (await this.registryService.isBuiltInSuppressed(manifest.manifest.id)) continue;
|
|
2946
3051
|
if ((await this.registryService.getApp(manifest.manifest.id))?.installedVersions[manifest.manifest.version]) continue;
|
|
2947
|
-
await this.installationService.install(appDirectory
|
|
3052
|
+
await this.installationService.install(appDirectory, { trustedPublisher: {
|
|
3053
|
+
id: "nextclaw",
|
|
3054
|
+
name: "NextClaw",
|
|
3055
|
+
url: "https://nextclaw.io"
|
|
3056
|
+
} });
|
|
2948
3057
|
}
|
|
2949
3058
|
};
|
|
2950
|
-
toPackageView = async (info) => {
|
|
2951
|
-
const activeVersion = info.installedVersions.find((version) => version.version ===
|
|
2952
|
-
if (!activeVersion) throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `应用 ${info.appId}
|
|
2953
|
-
const packagePresentation = await this.
|
|
3059
|
+
toPackageView = async (info, selectedVersion = info.activeVersion) => {
|
|
3060
|
+
const activeVersion = info.installedVersions.find((version) => version.version === selectedVersion);
|
|
3061
|
+
if (!activeVersion) throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `应用 ${info.appId} 缺少版本 ${selectedVersion}。`);
|
|
3062
|
+
const packagePresentation = await this.presentationService.readManifest(path.join(activeVersion.installDirectory, "manifest.json"));
|
|
3063
|
+
const manifestBundle = await this.manifestService.load(activeVersion.installDirectory);
|
|
3064
|
+
const security = manifestBundle.manifest.schemaVersion === 2 ? this.manifestService.resolvePlatformSecurity(manifestBundle.manifest) : {
|
|
3065
|
+
runtimeProfile: "wasi",
|
|
3066
|
+
isolation: manifestBundle.manifest.main.kind === "wasi-http-component" ? "host-mediated" : "sandboxed"
|
|
3067
|
+
};
|
|
2954
3068
|
return {
|
|
2955
3069
|
id: info.appId,
|
|
2956
3070
|
name: info.name,
|
|
@@ -2958,7 +3072,7 @@ var AppPackageManager = class {
|
|
|
2958
3072
|
icon: packagePresentation.icon,
|
|
2959
3073
|
nameI18n: packagePresentation.nameI18n,
|
|
2960
3074
|
descriptionI18n: packagePresentation.descriptionI18n,
|
|
2961
|
-
activeVersion:
|
|
3075
|
+
activeVersion: selectedVersion,
|
|
2962
3076
|
installedVersions: info.installedVersions.map((version) => version.version),
|
|
2963
3077
|
enabled: info.enabled,
|
|
2964
3078
|
builtIn: await this.isBuiltInAppId(info.appId),
|
|
@@ -2967,43 +3081,50 @@ var AppPackageManager = class {
|
|
|
2967
3081
|
kind: component.kind,
|
|
2968
3082
|
id: component.id,
|
|
2969
3083
|
packageId: info.appId,
|
|
2970
|
-
packageVersion:
|
|
3084
|
+
packageVersion: selectedVersion,
|
|
2971
3085
|
sourcePath: component.componentDirectory,
|
|
2972
3086
|
manifestPath: component.manifestPath,
|
|
2973
3087
|
dataDirectory: info.dataDirectory,
|
|
2974
|
-
|
|
3088
|
+
instanceId: info.instance.id,
|
|
3089
|
+
storage: info.storage,
|
|
3090
|
+
runtimeProfile: security.runtimeProfile,
|
|
3091
|
+
isolation: security.isolation,
|
|
3092
|
+
...await this.presentationService.readManifest(component.manifestPath)
|
|
2975
3093
|
}))),
|
|
2976
|
-
dataDirectory: info.dataDirectory
|
|
3094
|
+
dataDirectory: info.dataDirectory,
|
|
3095
|
+
instanceId: info.instance.id,
|
|
3096
|
+
storage: info.storage,
|
|
3097
|
+
storageUsage: info.storageUsage,
|
|
3098
|
+
runtimeProfile: security.runtimeProfile,
|
|
3099
|
+
isolation: security.isolation
|
|
2977
3100
|
};
|
|
2978
3101
|
};
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3102
|
+
resolveSecurity = (version, appId) => {
|
|
3103
|
+
if (version.security) return {
|
|
3104
|
+
runtimeProfile: version.security.runtimeProfile,
|
|
3105
|
+
isolation: version.security.isolation
|
|
3106
|
+
};
|
|
3107
|
+
const hasService = version.components?.some((component) => component.kind === "service") ?? false;
|
|
3108
|
+
if (version.manifestSchemaVersion !== 2) throw new AppPackageError("APP_PACKAGE_INCOMPATIBLE", `应用 ${appId} 仍使用 legacy schema,不能投影组件。`);
|
|
3109
|
+
return hasService ? {
|
|
3110
|
+
runtimeProfile: "native-process",
|
|
3111
|
+
isolation: "full-user"
|
|
3112
|
+
} : {
|
|
3113
|
+
runtimeProfile: "panel-only",
|
|
3114
|
+
isolation: "sandboxed"
|
|
2989
3115
|
};
|
|
2990
|
-
};
|
|
2991
|
-
readLocalizedField = (candidate, field) => {
|
|
2992
|
-
const value = candidate[field];
|
|
2993
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2994
|
-
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
2995
|
-
return entries.length > 0 ? { [field]: Object.fromEntries(entries) } : {};
|
|
2996
3116
|
};
|
|
2997
3117
|
toComponentSources = (app) => app.components.map((component) => ({ ...component }));
|
|
2998
|
-
assertEngineCompatibility = async (appId) => {
|
|
3118
|
+
assertEngineCompatibility = async (appId, selectedVersion) => {
|
|
2999
3119
|
const productVersion = this.params.productVersion?.trim();
|
|
3000
3120
|
if (!productVersion) return;
|
|
3001
3121
|
const info = await this.installationService.info(appId);
|
|
3002
|
-
const
|
|
3003
|
-
|
|
3122
|
+
const targetVersion = selectedVersion ?? info.activeVersion;
|
|
3123
|
+
const activeVersion = info.installedVersions.find((version) => version.version === targetVersion);
|
|
3124
|
+
if (!activeVersion) throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `应用 ${appId} 缺少版本 ${targetVersion}。`);
|
|
3004
3125
|
const manifestBundle = await this.manifestService.load(activeVersion.installDirectory);
|
|
3005
3126
|
const engineRange = manifestBundle.manifest.schemaVersion === 2 ? manifestBundle.manifest.engines?.nextclaw?.trim() : void 0;
|
|
3006
|
-
if (engineRange && !satisfiesAppEngineVersion(productVersion, engineRange)) throw new AppPackageError("APP_PACKAGE_INCOMPATIBLE", `应用 ${appId}@${
|
|
3127
|
+
if (engineRange && !satisfiesAppEngineVersion(productVersion, engineRange)) throw new AppPackageError("APP_PACKAGE_INCOMPATIBLE", `应用 ${appId}@${targetVersion} 要求 NextClaw ${engineRange},当前版本为 ${productVersion}。`);
|
|
3007
3128
|
};
|
|
3008
3129
|
listBuiltInDefinitions = async () => {
|
|
3009
3130
|
if (!this.params.builtInAppsDirectory) return [];
|
|
@@ -3027,29 +3148,6 @@ var AppPackageManager = class {
|
|
|
3027
3148
|
return await this.builtInDefinitionsPromise;
|
|
3028
3149
|
};
|
|
3029
3150
|
isBuiltInAppId = async (appId) => (await this.listBuiltInDefinitions()).some(({ manifest }) => manifest.manifest.id === appId);
|
|
3030
|
-
resolvePresentationIcon = async (manifestPath, icon) => {
|
|
3031
|
-
if (icon.startsWith("data:") || icon.startsWith("http://") || icon.startsWith("https://") || icon.startsWith("/") || !icon.includes("/") && !icon.includes(".") && [...icon].length <= 8) return icon;
|
|
3032
|
-
const manifestDirectory = path.dirname(manifestPath);
|
|
3033
|
-
const iconPath = path.resolve(manifestDirectory, icon);
|
|
3034
|
-
const relative = path.relative(manifestDirectory, iconPath);
|
|
3035
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) return icon;
|
|
3036
|
-
const mimeType = this.iconMimeType(path.extname(iconPath));
|
|
3037
|
-
if (!mimeType) return icon;
|
|
3038
|
-
const bytes = await readFile(iconPath);
|
|
3039
|
-
if (bytes.byteLength > 256 * 1024) return icon;
|
|
3040
|
-
return `data:${mimeType};base64,${bytes.toString("base64")}`;
|
|
3041
|
-
};
|
|
3042
|
-
iconMimeType = (extension) => {
|
|
3043
|
-
switch (extension.toLowerCase()) {
|
|
3044
|
-
case ".svg": return "image/svg+xml";
|
|
3045
|
-
case ".png": return "image/png";
|
|
3046
|
-
case ".jpg":
|
|
3047
|
-
case ".jpeg": return "image/jpeg";
|
|
3048
|
-
case ".webp": return "image/webp";
|
|
3049
|
-
case ".gif": return "image/gif";
|
|
3050
|
-
default: return;
|
|
3051
|
-
}
|
|
3052
|
-
};
|
|
3053
3151
|
executeOperation = async (input, report) => {
|
|
3054
3152
|
if (input.action === "install") {
|
|
3055
3153
|
const installed = await this.install(input.source, input.registryUrl, async (phase) => await report(phase));
|
|
@@ -3094,6 +3192,148 @@ var AppPackageManager = class {
|
|
|
3094
3192
|
isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3095
3193
|
};
|
|
3096
3194
|
//#endregion
|
|
3195
|
+
//#region src/types/app-data.types.ts
|
|
3196
|
+
var AppDataError = class extends Error {
|
|
3197
|
+
constructor(code, message) {
|
|
3198
|
+
super(message);
|
|
3199
|
+
this.code = code;
|
|
3200
|
+
this.name = "AppDataError";
|
|
3201
|
+
}
|
|
3202
|
+
};
|
|
3203
|
+
function isAppDataError(error) {
|
|
3204
|
+
return error instanceof AppDataError;
|
|
3205
|
+
}
|
|
3206
|
+
//#endregion
|
|
3207
|
+
//#region src/managers/app-data.manager.ts
|
|
3208
|
+
const DATA_ID_PREFIX = "ad1.";
|
|
3209
|
+
const SAFE_ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
3210
|
+
const WORKSPACE_SCOPE_PATTERN = /^[a-f0-9]{16}$/;
|
|
3211
|
+
var AppDataManager = class {
|
|
3212
|
+
appHomeService;
|
|
3213
|
+
fileLockService = new FileLockService();
|
|
3214
|
+
inventoryService = new AppInstanceInventoryService();
|
|
3215
|
+
constructor(params) {
|
|
3216
|
+
this.params = params;
|
|
3217
|
+
this.appHomeService = new AppHomeService(params.appHomeDirectory);
|
|
3218
|
+
}
|
|
3219
|
+
list = async () => {
|
|
3220
|
+
const workspacePath = path.resolve(this.params.getWorkspacePath());
|
|
3221
|
+
const workspaceInstancesRoot = this.getWorkspaceInstancesRoot(workspacePath);
|
|
3222
|
+
const [packageInventory, workspaceInventory, packageList, workspaceOwners] = await Promise.all([
|
|
3223
|
+
this.inventoryService.list(this.appHomeService.getInstancesDirectory()),
|
|
3224
|
+
this.inventoryService.list(workspaceInstancesRoot),
|
|
3225
|
+
this.params.listInstalledPackageOwners(),
|
|
3226
|
+
this.params.listWorkspaceDataOwners()
|
|
3227
|
+
]);
|
|
3228
|
+
const packageOwners = new Map(packageList.map((entry) => [entry.id, entry]));
|
|
3229
|
+
const workspaceOwnerMap = new Map(workspaceOwners.map((entry) => [entry.id, entry]));
|
|
3230
|
+
const workspaceScope = this.workspaceScope(workspacePath);
|
|
3231
|
+
return {
|
|
3232
|
+
entries: [...packageInventory.entries.map((entry) => this.toEntry({
|
|
3233
|
+
entry,
|
|
3234
|
+
source: "package",
|
|
3235
|
+
displayName: packageOwners.get(entry.appId)?.name ?? entry.appId,
|
|
3236
|
+
active: packageOwners.has(entry.appId)
|
|
3237
|
+
})), ...workspaceInventory.entries.map((entry) => this.toEntry({
|
|
3238
|
+
entry,
|
|
3239
|
+
source: "workspace-service",
|
|
3240
|
+
displayName: workspaceOwnerMap.get(entry.appId)?.title ?? entry.appId,
|
|
3241
|
+
active: workspaceOwnerMap.has(entry.appId),
|
|
3242
|
+
scope: workspaceScope
|
|
3243
|
+
}))].sort((left, right) => left.displayName.localeCompare(right.displayName) || left.id.localeCompare(right.id)),
|
|
3244
|
+
diagnostics: [...packageInventory.diagnostics.map((entry) => ({
|
|
3245
|
+
...entry,
|
|
3246
|
+
source: "package"
|
|
3247
|
+
})), ...workspaceInventory.diagnostics.map((entry) => ({
|
|
3248
|
+
...entry,
|
|
3249
|
+
source: "workspace-service"
|
|
3250
|
+
}))]
|
|
3251
|
+
};
|
|
3252
|
+
};
|
|
3253
|
+
deleteRetained = async (dataId, confirmAppId) => {
|
|
3254
|
+
const payload = this.parseDataId(dataId);
|
|
3255
|
+
if (confirmAppId !== payload.appId) throw new AppDataError("APP_DATA_CONFIRMATION_MISMATCH", `确认的 App id 与目标不一致:${confirmAppId || "(empty)"}`);
|
|
3256
|
+
if (payload.source === "package") await this.deletePackageData(payload);
|
|
3257
|
+
else await this.deleteWorkspaceData(payload);
|
|
3258
|
+
return {
|
|
3259
|
+
deleted: true,
|
|
3260
|
+
id: dataId,
|
|
3261
|
+
appId: payload.appId,
|
|
3262
|
+
instanceId: payload.instanceId
|
|
3263
|
+
};
|
|
3264
|
+
};
|
|
3265
|
+
deletePackageData = async (payload) => {
|
|
3266
|
+
await this.fileLockService.withLock(this.appHomeService.getAppOperationLockPath(payload.appId), async () => {
|
|
3267
|
+
await this.inventoryService.purge({
|
|
3268
|
+
instancesRoot: this.appHomeService.getInstancesDirectory(),
|
|
3269
|
+
appId: payload.appId,
|
|
3270
|
+
instanceId: payload.instanceId,
|
|
3271
|
+
assertCanPurge: async () => {
|
|
3272
|
+
if (await this.isPackageActive(payload.appId)) throw new AppDataError("APP_DATA_ACTIVE", `应用 ${payload.appId} 仍已安装,请通过卸载入口处理其数据。`);
|
|
3273
|
+
}
|
|
3274
|
+
});
|
|
3275
|
+
}).catch((error) => this.rethrowMissing(error, payload));
|
|
3276
|
+
};
|
|
3277
|
+
deleteWorkspaceData = async (payload) => {
|
|
3278
|
+
const workspacePath = path.resolve(this.params.getWorkspacePath());
|
|
3279
|
+
if (payload.scope !== this.workspaceScope(workspacePath)) throw new AppDataError("APP_DATA_INVALID_ID", "App data id 不属于当前 workspace。");
|
|
3280
|
+
const lockPath = path.join(workspacePath, ".nextclaw", "locks", "service-apps", `${payload.appId}.lock`);
|
|
3281
|
+
await this.fileLockService.withLock(lockPath, async () => {
|
|
3282
|
+
await this.inventoryService.purge({
|
|
3283
|
+
instancesRoot: this.getWorkspaceInstancesRoot(workspacePath),
|
|
3284
|
+
appId: payload.appId,
|
|
3285
|
+
instanceId: payload.instanceId,
|
|
3286
|
+
assertCanPurge: async () => {
|
|
3287
|
+
if (await this.isWorkspaceServiceActive(payload.appId)) throw new AppDataError("APP_DATA_ACTIVE", `Service App ${payload.appId} 仍在 workspace 中,请通过 Service Apps 删除入口处理其数据。`);
|
|
3288
|
+
}
|
|
3289
|
+
});
|
|
3290
|
+
}).catch((error) => this.rethrowMissing(error, payload));
|
|
3291
|
+
};
|
|
3292
|
+
toEntry = (params) => {
|
|
3293
|
+
const { active, displayName, entry, scope, source } = params;
|
|
3294
|
+
return {
|
|
3295
|
+
id: this.createDataId({
|
|
3296
|
+
version: 1,
|
|
3297
|
+
source,
|
|
3298
|
+
appId: entry.appId,
|
|
3299
|
+
instanceId: entry.instanceId,
|
|
3300
|
+
scope
|
|
3301
|
+
}),
|
|
3302
|
+
appId: entry.appId,
|
|
3303
|
+
instanceId: entry.instanceId,
|
|
3304
|
+
publisherId: entry.publisherId,
|
|
3305
|
+
displayName,
|
|
3306
|
+
source,
|
|
3307
|
+
lifecycle: active ? "active" : "retained",
|
|
3308
|
+
storage: entry.storage,
|
|
3309
|
+
usage: entry.usage,
|
|
3310
|
+
createdAt: entry.createdAt,
|
|
3311
|
+
migratedAt: entry.migratedAt,
|
|
3312
|
+
actions: { deleteRetainedData: !active }
|
|
3313
|
+
};
|
|
3314
|
+
};
|
|
3315
|
+
createDataId = (payload) => `${DATA_ID_PREFIX}${Buffer.from(JSON.stringify(payload), "utf8").toString("base64url")}`;
|
|
3316
|
+
parseDataId = (dataId) => {
|
|
3317
|
+
try {
|
|
3318
|
+
if (!dataId.startsWith(DATA_ID_PREFIX)) throw new Error("prefix");
|
|
3319
|
+
const raw = JSON.parse(Buffer.from(dataId.slice(4), "base64url").toString("utf8"));
|
|
3320
|
+
if (raw.version !== 1 || raw.source !== "package" && raw.source !== "workspace-service" || typeof raw.appId !== "string" || !SAFE_ID_PATTERN.test(raw.appId) || typeof raw.instanceId !== "string" || !SAFE_ID_PATTERN.test(raw.instanceId) || raw.source === "package" && raw.scope !== void 0 || raw.source === "workspace-service" && (typeof raw.scope !== "string" || !WORKSPACE_SCOPE_PATTERN.test(raw.scope))) throw new Error("shape");
|
|
3321
|
+
return raw;
|
|
3322
|
+
} catch {
|
|
3323
|
+
throw new AppDataError("APP_DATA_INVALID_ID", "App data id 无效。");
|
|
3324
|
+
}
|
|
3325
|
+
};
|
|
3326
|
+
isPackageActive = async (appId) => (await this.params.listInstalledPackageOwners()).some((entry) => entry.id === appId);
|
|
3327
|
+
isWorkspaceServiceActive = async (appId) => (await this.params.listWorkspaceDataOwners()).some((entry) => entry.id === appId);
|
|
3328
|
+
getWorkspaceInstancesRoot = (workspacePath) => path.join(workspacePath, ".nextclaw", "app-instances");
|
|
3329
|
+
workspaceScope = (workspacePath) => createHash("sha256").update(workspacePath).digest("hex").slice(0, 16);
|
|
3330
|
+
rethrowMissing = (error, payload) => {
|
|
3331
|
+
if (error instanceof Error && (error.message.includes("metadata 不存在") || this.isMissingFileError(error))) throw new AppDataError("APP_DATA_NOT_FOUND", `未找到 App 数据:${payload.appId}/${payload.instanceId}`);
|
|
3332
|
+
throw error;
|
|
3333
|
+
};
|
|
3334
|
+
isMissingFileError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3335
|
+
};
|
|
3336
|
+
//#endregion
|
|
3097
3337
|
//#region src/managers/channel.manager.ts
|
|
3098
3338
|
var ChannelManager = class {
|
|
3099
3339
|
channels = {};
|
|
@@ -9731,7 +9971,7 @@ var McpServiceAppRuntimeService = class {
|
|
|
9731
9971
|
toMcpServerRecord = (app, manifest) => ({
|
|
9732
9972
|
name: app.id,
|
|
9733
9973
|
definition: {
|
|
9734
|
-
enabled:
|
|
9974
|
+
enabled: app.enabled,
|
|
9735
9975
|
transport: {
|
|
9736
9976
|
type: "stdio",
|
|
9737
9977
|
command: manifest.command,
|
|
@@ -9752,7 +9992,16 @@ var McpServiceAppRuntimeService = class {
|
|
|
9752
9992
|
});
|
|
9753
9993
|
createAppRuntimeEnv = (app) => {
|
|
9754
9994
|
const env = {};
|
|
9755
|
-
if (app.
|
|
9995
|
+
if (app.storage) {
|
|
9996
|
+
env.NEXTCLAW_APP_INSTANCE_ID = app.storage.instanceId;
|
|
9997
|
+
env.NEXTCLAW_APP_COMPONENT_ID = app.id;
|
|
9998
|
+
env.NEXTCLAW_APP_DATA_DIR = app.storage.dataDirectory;
|
|
9999
|
+
env.NEXTCLAW_APP_CONFIG_DIR = app.storage.configDirectory;
|
|
10000
|
+
env.NEXTCLAW_APP_STATE_DIR = app.storage.stateDirectory;
|
|
10001
|
+
env.NEXTCLAW_APP_CACHE_DIR = app.storage.cacheDirectory;
|
|
10002
|
+
env.NEXTCLAW_APP_TMP_DIR = app.storage.temporaryDirectory;
|
|
10003
|
+
env.NEXTCLAW_APP_LOG_DIR = app.storage.logsDirectory;
|
|
10004
|
+
} else if (app.dataDirectory) env.NEXTCLAW_APP_DATA_DIR = app.dataDirectory;
|
|
9756
10005
|
if (app.sourceKind !== "package" || !app.packageId || !app.packageVersion || !app.packageDirectory) return env;
|
|
9757
10006
|
env.NEXTCLAW_APP_ID = app.packageId;
|
|
9758
10007
|
env.NEXTCLAW_APP_VERSION = app.packageVersion;
|
|
@@ -9775,6 +10024,261 @@ var McpServiceAppRuntimeService = class {
|
|
|
9775
10024
|
};
|
|
9776
10025
|
};
|
|
9777
10026
|
//#endregion
|
|
10027
|
+
//#region src/utils/service-app-manifest.utils.ts
|
|
10028
|
+
const SERVICE_APP_ID_PATTERN$1 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
10029
|
+
const SERVICE_ACTION_RISKS = new Set([
|
|
10030
|
+
"read",
|
|
10031
|
+
"write",
|
|
10032
|
+
"external",
|
|
10033
|
+
"dangerous"
|
|
10034
|
+
]);
|
|
10035
|
+
const SERVICE_APP_MANIFEST_FILE_NAME = "service-app.json";
|
|
10036
|
+
function getServiceAppManifestPath(dirPath) {
|
|
10037
|
+
return join(dirPath, SERVICE_APP_MANIFEST_FILE_NAME);
|
|
10038
|
+
}
|
|
10039
|
+
async function readServiceAppManifest(dirPath) {
|
|
10040
|
+
return parseServiceAppManifest(await readFile(getServiceAppManifestPath(dirPath), "utf8"));
|
|
10041
|
+
}
|
|
10042
|
+
function parseServiceAppManifest(raw) {
|
|
10043
|
+
let parsed;
|
|
10044
|
+
try {
|
|
10045
|
+
parsed = JSON.parse(raw);
|
|
10046
|
+
} catch (error) {
|
|
10047
|
+
throw new Error(`service-app.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
10048
|
+
}
|
|
10049
|
+
if (!isRecord$9(parsed)) throw new Error("service-app.json must contain an object.");
|
|
10050
|
+
const id = readRequiredString$6(parsed, "id");
|
|
10051
|
+
if (!SERVICE_APP_ID_PATTERN$1.test(id)) throw new Error("service app id must be kebab-case.");
|
|
10052
|
+
const protocol = readOptionalString$7(parsed, "protocol") ?? "mcp";
|
|
10053
|
+
if (protocol !== "mcp") throw new Error("service app protocol must be mcp.");
|
|
10054
|
+
return {
|
|
10055
|
+
id,
|
|
10056
|
+
title: readRequiredString$6(parsed, "title"),
|
|
10057
|
+
description: readOptionalString$7(parsed, "description"),
|
|
10058
|
+
enabled: readOptionalBoolean$1(parsed, "enabled") ?? true,
|
|
10059
|
+
protocol,
|
|
10060
|
+
command: readRequiredString$6(parsed, "command"),
|
|
10061
|
+
args: readStringArray(parsed.args, "args"),
|
|
10062
|
+
actions: readManifestActions(parsed.actions)
|
|
10063
|
+
};
|
|
10064
|
+
}
|
|
10065
|
+
function readManifestActions(value) {
|
|
10066
|
+
if (value === void 0) throw new Error("service app actions are required.");
|
|
10067
|
+
if (!isRecord$9(value)) throw new Error("service app actions must be an object.");
|
|
10068
|
+
if (Object.keys(value).length === 0) throw new Error("service app actions cannot be empty.");
|
|
10069
|
+
const actions = {};
|
|
10070
|
+
for (const [name, action] of Object.entries(value)) {
|
|
10071
|
+
if (!name.trim()) throw new Error("service app action name cannot be empty.");
|
|
10072
|
+
if (!isRecord$9(action)) throw new Error(`service app action ${name} must be an object.`);
|
|
10073
|
+
const risk = readOptionalString$7(action, "risk");
|
|
10074
|
+
if (risk !== void 0 && !SERVICE_ACTION_RISKS.has(risk)) throw new Error(`service app action ${name} has invalid risk.`);
|
|
10075
|
+
const inputSchema = action.inputSchema;
|
|
10076
|
+
if (inputSchema !== void 0 && !isRecord$9(inputSchema)) throw new Error(`service app action ${name} inputSchema must be an object.`);
|
|
10077
|
+
actions[name] = {
|
|
10078
|
+
risk,
|
|
10079
|
+
title: readOptionalString$7(action, "title"),
|
|
10080
|
+
description: readOptionalString$7(action, "description"),
|
|
10081
|
+
inputSchema
|
|
10082
|
+
};
|
|
10083
|
+
}
|
|
10084
|
+
return actions;
|
|
10085
|
+
}
|
|
10086
|
+
function readRequiredString$6(record, key) {
|
|
10087
|
+
const value = readOptionalString$7(record, key);
|
|
10088
|
+
if (!value) throw new Error(`service app ${key} is required.`);
|
|
10089
|
+
return value;
|
|
10090
|
+
}
|
|
10091
|
+
function readOptionalString$7(record, key) {
|
|
10092
|
+
return typeof record[key] === "string" && record[key].trim() ? record[key].trim() : void 0;
|
|
10093
|
+
}
|
|
10094
|
+
function readOptionalBoolean$1(record, key) {
|
|
10095
|
+
if (record[key] === void 0) return;
|
|
10096
|
+
if (typeof record[key] !== "boolean") throw new Error(`service app ${key} must be boolean.`);
|
|
10097
|
+
return record[key];
|
|
10098
|
+
}
|
|
10099
|
+
function readStringArray(value, key) {
|
|
10100
|
+
if (value === void 0) return [];
|
|
10101
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`service app ${key} must be a string array.`);
|
|
10102
|
+
return value;
|
|
10103
|
+
}
|
|
10104
|
+
function isRecord$9(value) {
|
|
10105
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10106
|
+
}
|
|
10107
|
+
//#endregion
|
|
10108
|
+
//#region src/services/service-app-record.service.ts
|
|
10109
|
+
var ServiceAppRecordService = class {
|
|
10110
|
+
instanceStorageService = new AppInstanceStorageService();
|
|
10111
|
+
constructor(params) {
|
|
10112
|
+
this.params = params;
|
|
10113
|
+
}
|
|
10114
|
+
buildWorkspaceRecord = async (serviceAppsPath, dirName) => {
|
|
10115
|
+
const dirPath = join(serviceAppsPath, dirName);
|
|
10116
|
+
try {
|
|
10117
|
+
const manifest = await readServiceAppManifest(dirPath);
|
|
10118
|
+
return this.fromManifest(dirPath, manifest, void 0, await this.materializeWorkspaceStorage(manifest.id));
|
|
10119
|
+
} catch (error) {
|
|
10120
|
+
if (this.isMissingFileError(error)) return null;
|
|
10121
|
+
return this.failedWorkspaceRecord(dirName, dirPath, error);
|
|
10122
|
+
}
|
|
10123
|
+
};
|
|
10124
|
+
buildPackageRecord = async (source) => {
|
|
10125
|
+
try {
|
|
10126
|
+
const manifest = await readServiceAppManifest(source.sourcePath);
|
|
10127
|
+
if (manifest.id !== source.id) throw new Error(`service component id mismatch: ${source.id}`);
|
|
10128
|
+
return this.fromManifest(source.sourcePath, manifest, source, source.storage);
|
|
10129
|
+
} catch (error) {
|
|
10130
|
+
return this.failedPackageRecord(source, error);
|
|
10131
|
+
}
|
|
10132
|
+
};
|
|
10133
|
+
listWorkspaceDataOwners = async (serviceAppsPath, dirNames) => {
|
|
10134
|
+
return (await Promise.all(dirNames.map(async (dirName) => {
|
|
10135
|
+
try {
|
|
10136
|
+
const manifest = await readServiceAppManifest(join(serviceAppsPath, dirName));
|
|
10137
|
+
return manifest.id === dirName ? {
|
|
10138
|
+
id: manifest.id,
|
|
10139
|
+
title: manifest.title
|
|
10140
|
+
} : null;
|
|
10141
|
+
} catch {
|
|
10142
|
+
return null;
|
|
10143
|
+
}
|
|
10144
|
+
}))).filter((entry) => Boolean(entry));
|
|
10145
|
+
};
|
|
10146
|
+
materializeWorkspaceStorage = async (serviceId) => {
|
|
10147
|
+
const instanceDirectory = join(this.params.getWorkspacePath(), ".nextclaw", "app-instances", serviceId, "default");
|
|
10148
|
+
return (await this.instanceStorageService.materialize({
|
|
10149
|
+
appId: serviceId,
|
|
10150
|
+
instanceId: "default",
|
|
10151
|
+
instanceDirectory
|
|
10152
|
+
})).storage;
|
|
10153
|
+
};
|
|
10154
|
+
fromManifest = (dirPath, manifest, packageSource, storage) => {
|
|
10155
|
+
const runtimeStatus = this.params.runtimeService.getStatus(manifest.id);
|
|
10156
|
+
return {
|
|
10157
|
+
id: manifest.id,
|
|
10158
|
+
title: manifest.title,
|
|
10159
|
+
description: manifest.description,
|
|
10160
|
+
dirPath,
|
|
10161
|
+
manifestPath: getServiceAppManifestPath(dirPath),
|
|
10162
|
+
command: manifest.command,
|
|
10163
|
+
args: manifest.args,
|
|
10164
|
+
cwd: dirPath,
|
|
10165
|
+
enabled: packageSource ? true : manifest.enabled,
|
|
10166
|
+
protocol: manifest.protocol,
|
|
10167
|
+
status: packageSource || manifest.enabled ? runtimeStatus.status : "stopped",
|
|
10168
|
+
lastError: runtimeStatus.lastError,
|
|
10169
|
+
lastStartedAt: runtimeStatus.lastStartedAt,
|
|
10170
|
+
lastReadyAt: runtimeStatus.lastReadyAt,
|
|
10171
|
+
lastFailedAt: runtimeStatus.lastFailedAt,
|
|
10172
|
+
sourceKind: packageSource ? "package" : "workspace",
|
|
10173
|
+
packageId: packageSource?.packageId,
|
|
10174
|
+
packageVersion: packageSource?.packageVersion,
|
|
10175
|
+
packageDirectory: packageSource ? join(packageSource.sourcePath, "..", "..") : void 0,
|
|
10176
|
+
dataDirectory: storage?.dataDirectory,
|
|
10177
|
+
instanceId: packageSource?.instanceId ?? storage?.instanceId,
|
|
10178
|
+
storage,
|
|
10179
|
+
isolation: packageSource?.isolation ?? "full-user"
|
|
10180
|
+
};
|
|
10181
|
+
};
|
|
10182
|
+
failedWorkspaceRecord = (id, dirPath, error) => ({
|
|
10183
|
+
id,
|
|
10184
|
+
title: this.toTitle(id),
|
|
10185
|
+
dirPath,
|
|
10186
|
+
manifestPath: getServiceAppManifestPath(dirPath),
|
|
10187
|
+
cwd: dirPath,
|
|
10188
|
+
enabled: false,
|
|
10189
|
+
protocol: "mcp",
|
|
10190
|
+
status: "failed",
|
|
10191
|
+
lastError: error instanceof Error ? error.message : String(error)
|
|
10192
|
+
});
|
|
10193
|
+
failedPackageRecord = (source, error) => ({
|
|
10194
|
+
id: source.id,
|
|
10195
|
+
title: this.toTitle(source.id),
|
|
10196
|
+
dirPath: source.sourcePath,
|
|
10197
|
+
manifestPath: source.manifestPath,
|
|
10198
|
+
cwd: source.sourcePath,
|
|
10199
|
+
enabled: false,
|
|
10200
|
+
protocol: "mcp",
|
|
10201
|
+
status: "failed",
|
|
10202
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
10203
|
+
sourceKind: "package",
|
|
10204
|
+
packageId: source.packageId,
|
|
10205
|
+
packageVersion: source.packageVersion,
|
|
10206
|
+
packageDirectory: join(source.sourcePath, "..", ".."),
|
|
10207
|
+
dataDirectory: source.dataDirectory,
|
|
10208
|
+
instanceId: source.instanceId,
|
|
10209
|
+
storage: source.storage,
|
|
10210
|
+
isolation: source.isolation
|
|
10211
|
+
});
|
|
10212
|
+
toTitle = (value) => basename(value).replace(/[-_]+/g, " ").trim() || value;
|
|
10213
|
+
isMissingFileError = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
10214
|
+
};
|
|
10215
|
+
//#endregion
|
|
10216
|
+
//#region src/services/service-app-removal.service.ts
|
|
10217
|
+
var ServiceAppRemovalCleanupError = class extends Error {
|
|
10218
|
+
constructor(cause) {
|
|
10219
|
+
super(`临时目录清理失败:${cause instanceof Error ? cause.message : String(cause)}`);
|
|
10220
|
+
this.cause = cause;
|
|
10221
|
+
this.name = "ServiceAppRemovalCleanupError";
|
|
10222
|
+
}
|
|
10223
|
+
};
|
|
10224
|
+
var ServiceAppRemovalService = class {
|
|
10225
|
+
fileLockService = new FileLockService();
|
|
10226
|
+
remove = async (params) => await this.fileLockService.withLock(params.lockPath, async () => {
|
|
10227
|
+
const record = await params.loadRecord();
|
|
10228
|
+
await this.removeLocked({
|
|
10229
|
+
...params,
|
|
10230
|
+
record
|
|
10231
|
+
});
|
|
10232
|
+
return record;
|
|
10233
|
+
});
|
|
10234
|
+
removeLocked = async (params) => {
|
|
10235
|
+
const { grantStore, purgeData, record, stopRuntime } = params;
|
|
10236
|
+
const grants = (await grantStore.list()).filter((grant) => grant.actionId.startsWith(`${record.id}.`));
|
|
10237
|
+
const stagedPaths = [];
|
|
10238
|
+
try {
|
|
10239
|
+
await stopRuntime(record);
|
|
10240
|
+
await this.stagePath(record.dirPath, stagedPaths);
|
|
10241
|
+
if (purgeData && record.storage) await this.stagePath(record.storage.instanceDirectory, stagedPaths);
|
|
10242
|
+
await grantStore.revokeActionsByPrefix(`${record.id}.`);
|
|
10243
|
+
} catch (error) {
|
|
10244
|
+
const recoveryErrors = [...await this.restoreStagedPaths(stagedPaths), ...await this.restoreGrants(grantStore, grants)];
|
|
10245
|
+
if (recoveryErrors.length > 0) throw new AggregateError([error, ...recoveryErrors], `Service App ${record.id} 删除失败,且恢复未完整完成。`);
|
|
10246
|
+
throw error;
|
|
10247
|
+
}
|
|
10248
|
+
try {
|
|
10249
|
+
await Promise.all(stagedPaths.map(async ({ stagedPath }) => await rm(stagedPath, { recursive: true })));
|
|
10250
|
+
} catch (error) {
|
|
10251
|
+
throw new ServiceAppRemovalCleanupError(error);
|
|
10252
|
+
}
|
|
10253
|
+
};
|
|
10254
|
+
stagePath = async (originalPath, stagedPaths) => {
|
|
10255
|
+
const stagedPath = `${originalPath}.deleting-${randomUUID()}`;
|
|
10256
|
+
await rename(originalPath, stagedPath);
|
|
10257
|
+
stagedPaths.push({
|
|
10258
|
+
originalPath,
|
|
10259
|
+
stagedPath
|
|
10260
|
+
});
|
|
10261
|
+
};
|
|
10262
|
+
restoreStagedPaths = async (stagedPaths) => {
|
|
10263
|
+
const errors = [];
|
|
10264
|
+
for (const entry of [...stagedPaths].reverse()) try {
|
|
10265
|
+
await rename(entry.stagedPath, entry.originalPath);
|
|
10266
|
+
} catch (error) {
|
|
10267
|
+
errors.push(error);
|
|
10268
|
+
}
|
|
10269
|
+
return errors;
|
|
10270
|
+
};
|
|
10271
|
+
restoreGrants = async (grantStore, grants) => {
|
|
10272
|
+
const errors = [];
|
|
10273
|
+
for (const grant of grants) try {
|
|
10274
|
+
await grantStore.grant(grant);
|
|
10275
|
+
} catch (error) {
|
|
10276
|
+
errors.push(error);
|
|
10277
|
+
}
|
|
10278
|
+
return errors;
|
|
10279
|
+
};
|
|
10280
|
+
};
|
|
10281
|
+
//#endregion
|
|
9778
10282
|
//#region src/stores/service-action-grant.store.ts
|
|
9779
10283
|
const EMPTY_GRANTS = {
|
|
9780
10284
|
version: 1,
|
|
@@ -9784,9 +10288,9 @@ var ServiceActionGrantStore = class {
|
|
|
9784
10288
|
constructor(storePath) {
|
|
9785
10289
|
this.storePath = storePath;
|
|
9786
10290
|
}
|
|
9787
|
-
isGranted = async (caller, actionId) => {
|
|
9788
|
-
const
|
|
9789
|
-
return Boolean(
|
|
10291
|
+
isGranted = async (caller, actionId, currentRisk) => {
|
|
10292
|
+
const grant = (await this.load()).grants[getServiceActionCallerKey(caller)]?.actions[actionId];
|
|
10293
|
+
return Boolean(grant && (currentRisk === void 0 || grant.risk === currentRisk));
|
|
9790
10294
|
};
|
|
9791
10295
|
grant = async ({ actionId, caller, grantedAt, risk }) => {
|
|
9792
10296
|
const data = await this.load();
|
|
@@ -9853,12 +10357,12 @@ var ServiceActionGrantStore = class {
|
|
|
9853
10357
|
};
|
|
9854
10358
|
};
|
|
9855
10359
|
function normalizeStoreData(value) {
|
|
9856
|
-
if (!isRecord$
|
|
10360
|
+
if (!isRecord$8(value) || value.version !== 1 || !isRecord$8(value.grants)) return structuredClone(EMPTY_GRANTS);
|
|
9857
10361
|
const grants = {};
|
|
9858
10362
|
for (const [callerKey, callerValue] of Object.entries(value.grants)) {
|
|
9859
|
-
if (!isRecord$
|
|
10363
|
+
if (!isRecord$8(callerValue) || !isRecord$8(callerValue.actions)) continue;
|
|
9860
10364
|
const actions = {};
|
|
9861
|
-
for (const [actionId, actionValue] of Object.entries(callerValue.actions)) if (isRecord$
|
|
10365
|
+
for (const [actionId, actionValue] of Object.entries(callerValue.actions)) if (isRecord$8(actionValue) && typeof actionValue.grantedAt === "string" && isServiceActionRisk(actionValue.risk)) actions[actionId] = {
|
|
9862
10366
|
grantedAt: actionValue.grantedAt,
|
|
9863
10367
|
risk: actionValue.risk
|
|
9864
10368
|
};
|
|
@@ -9872,92 +10376,11 @@ function normalizeStoreData(value) {
|
|
|
9872
10376
|
function isServiceActionRisk(value) {
|
|
9873
10377
|
return value === "read" || value === "write" || value === "external" || value === "dangerous";
|
|
9874
10378
|
}
|
|
9875
|
-
function isRecord$
|
|
10379
|
+
function isRecord$8(value) {
|
|
9876
10380
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9877
10381
|
}
|
|
9878
10382
|
function isMissingFileError(error) {
|
|
9879
|
-
return isRecord$
|
|
9880
|
-
}
|
|
9881
|
-
//#endregion
|
|
9882
|
-
//#region src/utils/service-app-manifest.utils.ts
|
|
9883
|
-
const SERVICE_APP_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9884
|
-
const SERVICE_ACTION_RISKS = new Set([
|
|
9885
|
-
"read",
|
|
9886
|
-
"write",
|
|
9887
|
-
"external",
|
|
9888
|
-
"dangerous"
|
|
9889
|
-
]);
|
|
9890
|
-
const SERVICE_APP_MANIFEST_FILE_NAME = "service-app.json";
|
|
9891
|
-
function getServiceAppManifestPath(dirPath) {
|
|
9892
|
-
return join(dirPath, SERVICE_APP_MANIFEST_FILE_NAME);
|
|
9893
|
-
}
|
|
9894
|
-
async function readServiceAppManifest(dirPath) {
|
|
9895
|
-
return parseServiceAppManifest(await readFile(getServiceAppManifestPath(dirPath), "utf8"));
|
|
9896
|
-
}
|
|
9897
|
-
function parseServiceAppManifest(raw) {
|
|
9898
|
-
let parsed;
|
|
9899
|
-
try {
|
|
9900
|
-
parsed = JSON.parse(raw);
|
|
9901
|
-
} catch (error) {
|
|
9902
|
-
throw new Error(`service-app.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
9903
|
-
}
|
|
9904
|
-
if (!isRecord$8(parsed)) throw new Error("service-app.json must contain an object.");
|
|
9905
|
-
const id = readRequiredString$6(parsed, "id");
|
|
9906
|
-
if (!SERVICE_APP_ID_PATTERN.test(id)) throw new Error("service app id must be kebab-case.");
|
|
9907
|
-
const protocol = readOptionalString$7(parsed, "protocol") ?? "mcp";
|
|
9908
|
-
if (protocol !== "mcp") throw new Error("service app protocol must be mcp.");
|
|
9909
|
-
return {
|
|
9910
|
-
id,
|
|
9911
|
-
title: readRequiredString$6(parsed, "title"),
|
|
9912
|
-
description: readOptionalString$7(parsed, "description"),
|
|
9913
|
-
enabled: readOptionalBoolean$1(parsed, "enabled") ?? true,
|
|
9914
|
-
protocol,
|
|
9915
|
-
command: readRequiredString$6(parsed, "command"),
|
|
9916
|
-
args: readStringArray(parsed.args, "args"),
|
|
9917
|
-
actions: readManifestActions(parsed.actions)
|
|
9918
|
-
};
|
|
9919
|
-
}
|
|
9920
|
-
function readManifestActions(value) {
|
|
9921
|
-
if (value === void 0) throw new Error("service app actions are required.");
|
|
9922
|
-
if (!isRecord$8(value)) throw new Error("service app actions must be an object.");
|
|
9923
|
-
if (Object.keys(value).length === 0) throw new Error("service app actions cannot be empty.");
|
|
9924
|
-
const actions = {};
|
|
9925
|
-
for (const [name, action] of Object.entries(value)) {
|
|
9926
|
-
if (!name.trim()) throw new Error("service app action name cannot be empty.");
|
|
9927
|
-
if (!isRecord$8(action)) throw new Error(`service app action ${name} must be an object.`);
|
|
9928
|
-
const risk = readOptionalString$7(action, "risk");
|
|
9929
|
-
if (risk !== void 0 && !SERVICE_ACTION_RISKS.has(risk)) throw new Error(`service app action ${name} has invalid risk.`);
|
|
9930
|
-
const inputSchema = action.inputSchema;
|
|
9931
|
-
if (inputSchema !== void 0 && !isRecord$8(inputSchema)) throw new Error(`service app action ${name} inputSchema must be an object.`);
|
|
9932
|
-
actions[name] = {
|
|
9933
|
-
risk,
|
|
9934
|
-
title: readOptionalString$7(action, "title"),
|
|
9935
|
-
description: readOptionalString$7(action, "description"),
|
|
9936
|
-
inputSchema
|
|
9937
|
-
};
|
|
9938
|
-
}
|
|
9939
|
-
return actions;
|
|
9940
|
-
}
|
|
9941
|
-
function readRequiredString$6(record, key) {
|
|
9942
|
-
const value = readOptionalString$7(record, key);
|
|
9943
|
-
if (!value) throw new Error(`service app ${key} is required.`);
|
|
9944
|
-
return value;
|
|
9945
|
-
}
|
|
9946
|
-
function readOptionalString$7(record, key) {
|
|
9947
|
-
return typeof record[key] === "string" && record[key].trim() ? record[key].trim() : void 0;
|
|
9948
|
-
}
|
|
9949
|
-
function readOptionalBoolean$1(record, key) {
|
|
9950
|
-
if (record[key] === void 0) return;
|
|
9951
|
-
if (typeof record[key] !== "boolean") throw new Error(`service app ${key} must be boolean.`);
|
|
9952
|
-
return record[key];
|
|
9953
|
-
}
|
|
9954
|
-
function readStringArray(value, key) {
|
|
9955
|
-
if (value === void 0) return [];
|
|
9956
|
-
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`service app ${key} must be a string array.`);
|
|
9957
|
-
return value;
|
|
9958
|
-
}
|
|
9959
|
-
function isRecord$8(value) {
|
|
9960
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10383
|
+
return isRecord$8(error) && error.code === "ENOENT";
|
|
9961
10384
|
}
|
|
9962
10385
|
//#endregion
|
|
9963
10386
|
//#region src/utils/service-app-runtime-action.utils.ts
|
|
@@ -9994,6 +10417,7 @@ function mergeServiceAppRuntimeActions({ record, manifest, runtimeActions }) {
|
|
|
9994
10417
|
//#endregion
|
|
9995
10418
|
//#region src/managers/service-app.manager.ts
|
|
9996
10419
|
const SERVICE_ACTION_GRANTS_FILE_NAME = ".service-action-grants.json";
|
|
10420
|
+
const SERVICE_APP_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9997
10421
|
var ServiceAppError = class extends Error {
|
|
9998
10422
|
constructor(code, message) {
|
|
9999
10423
|
super(message);
|
|
@@ -10005,18 +10429,24 @@ function isServiceAppError(error) {
|
|
|
10005
10429
|
return error instanceof ServiceAppError;
|
|
10006
10430
|
}
|
|
10007
10431
|
var ServiceAppManager = class {
|
|
10432
|
+
removalService = new ServiceAppRemovalService();
|
|
10008
10433
|
runtimeService;
|
|
10434
|
+
recordService;
|
|
10009
10435
|
constructor(params) {
|
|
10010
10436
|
this.params = params;
|
|
10011
10437
|
this.runtimeService = params.runtimeService ?? new McpServiceAppRuntimeService({ getConfig: () => params.configManager.config });
|
|
10438
|
+
this.recordService = new ServiceAppRecordService({
|
|
10439
|
+
getWorkspacePath: this.getWorkspacePath,
|
|
10440
|
+
runtimeService: this.runtimeService
|
|
10441
|
+
});
|
|
10012
10442
|
}
|
|
10013
10443
|
listServiceApps = async () => {
|
|
10014
10444
|
const workspacePath = this.getWorkspacePath();
|
|
10015
10445
|
const serviceAppsPath = this.getServiceAppsPath(workspacePath);
|
|
10016
10446
|
const dirNames = await this.listServiceAppDirNames(serviceAppsPath);
|
|
10017
|
-
const workspaceEntries = await Promise.all(dirNames.map((dirName) => this.
|
|
10447
|
+
const workspaceEntries = await Promise.all(dirNames.map((dirName) => this.recordService.buildWorkspaceRecord(serviceAppsPath, dirName)));
|
|
10018
10448
|
const packageSources = (await this.listPackageComponentSources()).filter((component) => component.kind === "service");
|
|
10019
|
-
const packageEntries = await Promise.all(packageSources.map((source) => this.
|
|
10449
|
+
const packageEntries = await Promise.all(packageSources.map((source) => this.recordService.buildPackageRecord(source)));
|
|
10020
10450
|
return {
|
|
10021
10451
|
workspacePath,
|
|
10022
10452
|
serviceAppsPath,
|
|
@@ -10048,7 +10478,8 @@ var ServiceAppManager = class {
|
|
|
10048
10478
|
const { manifest, record } = await this.requireServiceAppForAction(actionId);
|
|
10049
10479
|
const actionName = getServiceActionName(actionId, record.id);
|
|
10050
10480
|
if (!Object.hasOwn(manifest.actions, actionName)) throw new ServiceAppError("SERVICE_APP_ACTION_NOT_FOUND", "service action not found");
|
|
10051
|
-
|
|
10481
|
+
const declaredRisk = manifest.actions[actionName]?.risk ?? "dangerous";
|
|
10482
|
+
if (!await this.createGrantStore().isGranted(request.caller, actionId, declaredRisk)) throw new ServiceAppError("AUTHORIZATION_REQUIRED", `This panel app needs permission to call ${actionId}.`);
|
|
10052
10483
|
return {
|
|
10053
10484
|
actionId,
|
|
10054
10485
|
result: await this.runtimeService.invokeAction({
|
|
@@ -10096,20 +10527,35 @@ var ServiceAppManager = class {
|
|
|
10096
10527
|
await this.runtimeService.restart(record.id);
|
|
10097
10528
|
return await this.getServiceApp(appId);
|
|
10098
10529
|
};
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
if (
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10530
|
+
listWorkspaceDataOwners = async () => await this.recordService.listWorkspaceDataOwners(this.getServiceAppsPath(this.getWorkspacePath()), await this.listServiceAppDirNames(this.getServiceAppsPath(this.getWorkspacePath())));
|
|
10531
|
+
deleteServiceApp = async (appId, purgeData = false) => {
|
|
10532
|
+
if (!SERVICE_APP_ID_PATTERN.test(appId)) throw new ServiceAppError("SERVICE_APP_INVALID_MANIFEST", "service app id must use kebab-case");
|
|
10533
|
+
const workspacePath = this.getWorkspacePath();
|
|
10534
|
+
let record;
|
|
10535
|
+
try {
|
|
10536
|
+
record = await this.removalService.remove({
|
|
10537
|
+
grantStore: this.createGrantStore(),
|
|
10538
|
+
lockPath: join(workspacePath, ".nextclaw", "locks", "service-apps", `${appId}.lock`),
|
|
10539
|
+
purgeData,
|
|
10540
|
+
loadRecord: async () => {
|
|
10541
|
+
const { record } = await this.requireServiceApp(appId);
|
|
10542
|
+
if (record.sourceKind === "package") throw new ServiceAppError("SERVICE_APP_MANAGED_SOURCE", `package service must be managed through Apps: ${record.packageId}`);
|
|
10543
|
+
if (purgeData && !record.storage) throw new ServiceAppError("SERVICE_APP_READ_FAILED", `Service App ${appId} 缺少受管数据目录,已停止删除。`);
|
|
10544
|
+
return record;
|
|
10545
|
+
},
|
|
10546
|
+
stopRuntime: async (loaded) => await this.runtimeService.restart(loaded.id)
|
|
10547
|
+
});
|
|
10548
|
+
} catch (error) {
|
|
10549
|
+
if (error instanceof ServiceAppRemovalCleanupError) throw new ServiceAppError("SERVICE_APP_RUNTIME_FAILED", `Service App ${appId} 已从 workspace 移除,但${error.message}`);
|
|
10550
|
+
throw error;
|
|
10551
|
+
}
|
|
10105
10552
|
return {
|
|
10106
10553
|
deleted: true,
|
|
10107
|
-
id: record.id
|
|
10554
|
+
id: record.id,
|
|
10555
|
+
dataRemoved: purgeData
|
|
10108
10556
|
};
|
|
10109
10557
|
};
|
|
10110
|
-
dispose = async () =>
|
|
10111
|
-
await this.runtimeService.dispose();
|
|
10112
|
-
};
|
|
10558
|
+
dispose = async () => await this.runtimeService.dispose();
|
|
10113
10559
|
assertCanActivatePackageComponents = async (components) => {
|
|
10114
10560
|
const serviceComponents = components.filter((component) => component.kind === "service");
|
|
10115
10561
|
if (serviceComponents.length === 0) return;
|
|
@@ -10125,6 +10571,31 @@ var ServiceAppManager = class {
|
|
|
10125
10571
|
const conflictsWithPackage = activePackageSources.some((active) => active.kind === "service" && active.id === component.id && active.packageId !== component.packageId);
|
|
10126
10572
|
if (workspaceIds.has(component.id) || conflictsWithPackage) throw new AppPackageError("APP_PACKAGE_CONFLICT", `Service component id 冲突:${component.id}`);
|
|
10127
10573
|
}
|
|
10574
|
+
for (const component of serviceComponents) {
|
|
10575
|
+
let manifest;
|
|
10576
|
+
try {
|
|
10577
|
+
manifest = await readServiceAppManifest(component.sourcePath);
|
|
10578
|
+
} catch (error) {
|
|
10579
|
+
throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `Service component ${component.id} manifest 无效:${error instanceof Error ? error.message : String(error)}`);
|
|
10580
|
+
}
|
|
10581
|
+
try {
|
|
10582
|
+
const record = this.recordService.fromManifest(component.sourcePath, manifest, component, component.storage);
|
|
10583
|
+
const runtimeActions = await this.runtimeService.listActions({
|
|
10584
|
+
app: record,
|
|
10585
|
+
manifest
|
|
10586
|
+
});
|
|
10587
|
+
const runtimeStatus = this.runtimeService.getStatus(component.id);
|
|
10588
|
+
if (runtimeStatus.status === "failed") throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `Service component ${component.id} 启动探测失败:${runtimeStatus.lastError ?? "unknown error"}`);
|
|
10589
|
+
const invalidAction = mergeServiceAppRuntimeActions({
|
|
10590
|
+
record,
|
|
10591
|
+
manifest,
|
|
10592
|
+
runtimeActions
|
|
10593
|
+
}).find((action) => action.runtimeState === "missing" || action.runtimeState === "undeclared");
|
|
10594
|
+
if (invalidAction) throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `Service component ${component.id} action 合同不一致:${invalidAction.id} (${invalidAction.runtimeState})`);
|
|
10595
|
+
} finally {
|
|
10596
|
+
await this.runtimeService.restart(component.id);
|
|
10597
|
+
}
|
|
10598
|
+
}
|
|
10128
10599
|
};
|
|
10129
10600
|
deactivatePackageComponents = async (components) => {
|
|
10130
10601
|
const serviceIds = components.filter((component) => component.kind === "service").map((component) => component.id);
|
|
@@ -10137,7 +10608,7 @@ var ServiceAppManager = class {
|
|
|
10137
10608
|
};
|
|
10138
10609
|
withGrantState = async (action, params) => {
|
|
10139
10610
|
if (!params.caller) return action;
|
|
10140
|
-
const granted = await this.createGrantStore().isGranted(params.caller, action.id);
|
|
10611
|
+
const granted = await this.createGrantStore().isGranted(params.caller, action.id, action.risk);
|
|
10141
10612
|
return {
|
|
10142
10613
|
...action,
|
|
10143
10614
|
grantState: resolveServiceActionGrantState({
|
|
@@ -10175,7 +10646,7 @@ var ServiceAppManager = class {
|
|
|
10175
10646
|
if (manifest.id !== appId) throw new ServiceAppError("SERVICE_APP_INVALID_MANIFEST", "service app manifest id must match directory name");
|
|
10176
10647
|
return {
|
|
10177
10648
|
manifest,
|
|
10178
|
-
record: this.
|
|
10649
|
+
record: this.recordService.fromManifest(dirPath, manifest, packageSource, packageSource?.storage ?? await this.recordService.materializeWorkspaceStorage(manifest.id))
|
|
10179
10650
|
};
|
|
10180
10651
|
} catch (error) {
|
|
10181
10652
|
if (isServiceAppError(error)) throw error;
|
|
@@ -10193,7 +10664,7 @@ var ServiceAppManager = class {
|
|
|
10193
10664
|
const manifest = await readServiceAppManifest(dirPath);
|
|
10194
10665
|
return {
|
|
10195
10666
|
manifest,
|
|
10196
|
-
record: this.
|
|
10667
|
+
record: this.recordService.fromManifest(dirPath, manifest, void 0, await this.recordService.materializeWorkspaceStorage(manifest.id))
|
|
10197
10668
|
};
|
|
10198
10669
|
} catch {
|
|
10199
10670
|
return null;
|
|
@@ -10204,7 +10675,7 @@ var ServiceAppManager = class {
|
|
|
10204
10675
|
const manifest = await readServiceAppManifest(component.sourcePath);
|
|
10205
10676
|
return {
|
|
10206
10677
|
manifest,
|
|
10207
|
-
record: this.
|
|
10678
|
+
record: this.recordService.fromManifest(component.sourcePath, manifest, component, component.storage)
|
|
10208
10679
|
};
|
|
10209
10680
|
} catch {
|
|
10210
10681
|
return null;
|
|
@@ -10212,51 +10683,6 @@ var ServiceAppManager = class {
|
|
|
10212
10683
|
}));
|
|
10213
10684
|
return [...workspaceEntries, ...packageEntries].filter((entry) => Boolean(entry));
|
|
10214
10685
|
};
|
|
10215
|
-
buildServiceAppRecord = async (serviceAppsPath, dirName) => {
|
|
10216
|
-
const dirPath = join(serviceAppsPath, dirName);
|
|
10217
|
-
try {
|
|
10218
|
-
const manifest = await readServiceAppManifest(dirPath);
|
|
10219
|
-
return this.toServiceAppRecord(dirPath, manifest);
|
|
10220
|
-
} catch (error) {
|
|
10221
|
-
if (this.isMissingFileError(error)) return null;
|
|
10222
|
-
return {
|
|
10223
|
-
id: dirName,
|
|
10224
|
-
title: toTitle(dirName),
|
|
10225
|
-
dirPath,
|
|
10226
|
-
manifestPath: getServiceAppManifestPath(dirPath),
|
|
10227
|
-
cwd: dirPath,
|
|
10228
|
-
enabled: false,
|
|
10229
|
-
protocol: "mcp",
|
|
10230
|
-
status: "failed",
|
|
10231
|
-
lastError: error instanceof Error ? error.message : String(error)
|
|
10232
|
-
};
|
|
10233
|
-
}
|
|
10234
|
-
};
|
|
10235
|
-
toServiceAppRecord = (dirPath, manifest, packageSource) => {
|
|
10236
|
-
const runtimeStatus = this.runtimeService.getStatus(manifest.id);
|
|
10237
|
-
return {
|
|
10238
|
-
id: manifest.id,
|
|
10239
|
-
title: manifest.title,
|
|
10240
|
-
description: manifest.description,
|
|
10241
|
-
dirPath,
|
|
10242
|
-
manifestPath: getServiceAppManifestPath(dirPath),
|
|
10243
|
-
command: manifest.command,
|
|
10244
|
-
args: manifest.args,
|
|
10245
|
-
cwd: dirPath,
|
|
10246
|
-
enabled: manifest.enabled,
|
|
10247
|
-
protocol: manifest.protocol,
|
|
10248
|
-
status: manifest.enabled ? runtimeStatus.status : "stopped",
|
|
10249
|
-
lastError: runtimeStatus.lastError,
|
|
10250
|
-
lastStartedAt: runtimeStatus.lastStartedAt,
|
|
10251
|
-
lastReadyAt: runtimeStatus.lastReadyAt,
|
|
10252
|
-
lastFailedAt: runtimeStatus.lastFailedAt,
|
|
10253
|
-
sourceKind: packageSource ? "package" : "workspace",
|
|
10254
|
-
packageId: packageSource?.packageId,
|
|
10255
|
-
packageVersion: packageSource?.packageVersion,
|
|
10256
|
-
packageDirectory: packageSource ? join(packageSource.sourcePath, "..", "..") : void 0,
|
|
10257
|
-
dataDirectory: packageSource?.dataDirectory
|
|
10258
|
-
};
|
|
10259
|
-
};
|
|
10260
10686
|
assertCaller = (caller) => {
|
|
10261
10687
|
if (caller.surface !== "panel-app" || !caller.appId.trim()) throw new ServiceAppError("SERVICE_APP_INVALID_CALLER", "service action caller is invalid");
|
|
10262
10688
|
};
|
|
@@ -10268,30 +10694,6 @@ var ServiceAppManager = class {
|
|
|
10268
10694
|
getServiceAppsPath = (workspacePath) => join(workspacePath, DEFAULT_SERVICE_APPS_DIR);
|
|
10269
10695
|
createGrantStore = () => new ServiceActionGrantStore(join(this.getServiceAppsPath(this.getWorkspacePath()), SERVICE_ACTION_GRANTS_FILE_NAME));
|
|
10270
10696
|
listPackageComponentSources = async () => await this.params.listPackageComponentSources?.() ?? [];
|
|
10271
|
-
buildServiceAppRecordFromPackage = async (source) => {
|
|
10272
|
-
try {
|
|
10273
|
-
const manifest = await readServiceAppManifest(source.sourcePath);
|
|
10274
|
-
if (manifest.id !== source.id) throw new Error(`service component id mismatch: ${source.id}`);
|
|
10275
|
-
return this.toServiceAppRecord(source.sourcePath, manifest, source);
|
|
10276
|
-
} catch (error) {
|
|
10277
|
-
return {
|
|
10278
|
-
id: source.id,
|
|
10279
|
-
title: toTitle(source.id),
|
|
10280
|
-
dirPath: source.sourcePath,
|
|
10281
|
-
manifestPath: source.manifestPath,
|
|
10282
|
-
cwd: source.sourcePath,
|
|
10283
|
-
enabled: false,
|
|
10284
|
-
protocol: "mcp",
|
|
10285
|
-
status: "failed",
|
|
10286
|
-
lastError: error instanceof Error ? error.message : String(error),
|
|
10287
|
-
sourceKind: "package",
|
|
10288
|
-
packageId: source.packageId,
|
|
10289
|
-
packageVersion: source.packageVersion,
|
|
10290
|
-
packageDirectory: join(source.sourcePath, "..", ".."),
|
|
10291
|
-
dataDirectory: source.dataDirectory
|
|
10292
|
-
};
|
|
10293
|
-
}
|
|
10294
|
-
};
|
|
10295
10697
|
listServiceAppDirNames = async (serviceAppsPath) => {
|
|
10296
10698
|
try {
|
|
10297
10699
|
return (await readdir(serviceAppsPath, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
@@ -10302,9 +10704,6 @@ var ServiceAppManager = class {
|
|
|
10302
10704
|
};
|
|
10303
10705
|
isMissingFileError = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
10304
10706
|
};
|
|
10305
|
-
function toTitle(value) {
|
|
10306
|
-
return basename(value).replace(/[-_]+/g, " ").trim() || value;
|
|
10307
|
-
}
|
|
10308
10707
|
//#endregion
|
|
10309
10708
|
//#region src/managers/session-run.manager.ts
|
|
10310
10709
|
var MessageInbox = class {
|
|
@@ -15358,35 +15757,48 @@ var ToolProviderContribution = class {
|
|
|
15358
15757
|
};
|
|
15359
15758
|
};
|
|
15360
15759
|
//#endregion
|
|
15361
|
-
//#region src/app/
|
|
15760
|
+
//#region src/app/kernel-storage-paths.ts
|
|
15362
15761
|
function resolveKernelAppHomeDirectory(options) {
|
|
15363
15762
|
const homeDir = options.homeDir?.trim();
|
|
15364
15763
|
return resolve(homeDir ? expandHome(homeDir) : getDataDir(), "apps");
|
|
15365
15764
|
}
|
|
15366
15765
|
function resolveKernelSessionsDir(options) {
|
|
15367
15766
|
const homeDir = options.homeDir?.trim();
|
|
15368
|
-
|
|
15369
|
-
return getSessionsPath();
|
|
15767
|
+
return homeDir ? ensureDir(resolve(expandHome(homeDir), "sessions")) : getSessionsPath();
|
|
15370
15768
|
}
|
|
15371
15769
|
function resolveKernelAutomationStorePath(options) {
|
|
15372
|
-
|
|
15373
|
-
if (homeDir) return resolve(expandHome(homeDir), "cron", "jobs.json");
|
|
15374
|
-
return resolve(getDataDir(), "cron", "jobs.json");
|
|
15770
|
+
return resolveKernelDataPath(options, "cron", "jobs.json");
|
|
15375
15771
|
}
|
|
15376
15772
|
function resolveKernelPreferenceStorePath(options) {
|
|
15377
|
-
|
|
15378
|
-
if (homeDir) return resolve(expandHome(homeDir), "preferences", "preferences.json");
|
|
15379
|
-
return resolve(getDataDir(), "preferences", "preferences.json");
|
|
15773
|
+
return resolveKernelDataPath(options, "preferences", "preferences.json");
|
|
15380
15774
|
}
|
|
15381
15775
|
function resolveKernelProjectStorePath(options) {
|
|
15382
|
-
|
|
15383
|
-
if (homeDir) return resolve(expandHome(homeDir), "projects", "projects.json");
|
|
15384
|
-
return resolve(getDataDir(), "projects", "projects.json");
|
|
15776
|
+
return resolveKernelDataPath(options, "projects", "projects.json");
|
|
15385
15777
|
}
|
|
15386
15778
|
function resolveKernelInboxDeliveryStorePath(options) {
|
|
15779
|
+
return resolveKernelDataPath(options, "inbox", "deliveries.json");
|
|
15780
|
+
}
|
|
15781
|
+
function resolveKernelDataPath(options, ...segments) {
|
|
15387
15782
|
const homeDir = options.homeDir?.trim();
|
|
15388
|
-
|
|
15389
|
-
|
|
15783
|
+
return resolve(homeDir ? expandHome(homeDir) : getDataDir(), ...segments);
|
|
15784
|
+
}
|
|
15785
|
+
//#endregion
|
|
15786
|
+
//#region src/app/nextclaw-kernel.ts
|
|
15787
|
+
function createKernelServiceAppManagers(params) {
|
|
15788
|
+
const { appHomeDirectory, appPackageManager, configManager } = params;
|
|
15789
|
+
const serviceAppManager = new ServiceAppManager({
|
|
15790
|
+
configManager,
|
|
15791
|
+
listPackageComponentSources: appPackageManager.listActiveComponentSources
|
|
15792
|
+
});
|
|
15793
|
+
return {
|
|
15794
|
+
serviceAppManager,
|
|
15795
|
+
appDataManager: new AppDataManager({
|
|
15796
|
+
appHomeDirectory,
|
|
15797
|
+
getWorkspacePath: () => getWorkspacePathFromConfig(configManager.config),
|
|
15798
|
+
listInstalledPackageOwners: appPackageManager.listInstalledDataOwners,
|
|
15799
|
+
listWorkspaceDataOwners: serviceAppManager.listWorkspaceDataOwners
|
|
15800
|
+
})
|
|
15801
|
+
};
|
|
15390
15802
|
}
|
|
15391
15803
|
var NextclawKernelControlManager = class {
|
|
15392
15804
|
runtimeControl = null;
|
|
@@ -15412,6 +15824,7 @@ var NextclawKernel = class {
|
|
|
15412
15824
|
skills;
|
|
15413
15825
|
automation;
|
|
15414
15826
|
appPackageManager;
|
|
15827
|
+
appDataManager;
|
|
15415
15828
|
channels;
|
|
15416
15829
|
sessionRequests;
|
|
15417
15830
|
sessionSearch;
|
|
@@ -15489,10 +15902,11 @@ var NextclawKernel = class {
|
|
|
15489
15902
|
listPackageComponentSources: this.appPackageManager.listActiveComponentSources
|
|
15490
15903
|
});
|
|
15491
15904
|
this.preferenceManager = new PreferenceManager({ storePath: resolveKernelPreferenceStorePath(options) });
|
|
15492
|
-
this.serviceAppManager =
|
|
15493
|
-
|
|
15494
|
-
|
|
15495
|
-
|
|
15905
|
+
({appDataManager: this.appDataManager, serviceAppManager: this.serviceAppManager} = createKernelServiceAppManagers({
|
|
15906
|
+
appHomeDirectory: resolveKernelAppHomeDirectory(options),
|
|
15907
|
+
appPackageManager: this.appPackageManager,
|
|
15908
|
+
configManager: this.configManager
|
|
15909
|
+
}));
|
|
15496
15910
|
this.installAppPackageRuntimeHooks();
|
|
15497
15911
|
this.extensions = new ExtensionManager({
|
|
15498
15912
|
configManager: this.configManager,
|
|
@@ -16149,6 +16563,6 @@ function resolveLegacyEventType(message) {
|
|
|
16149
16563
|
return `message.${role || "other"}`;
|
|
16150
16564
|
}
|
|
16151
16565
|
//#endregion
|
|
16152
|
-
export { AUTOMATIC_UPDATE_CHECK_INTERVAL_MS, AccessManager, AgentManager, AgentRunClient, AppPackageError, AppPackageManager, AutomationManager, BuiltinNarpRuntimeProviderService, CONTEXT_COMPACTION_CONTINUATION_TEXT, CONTEXT_COMPACTION_PROJECTION_KIND, CONTEXT_COMPACTION_PROJECTION_METADATA_KEY, CONTEXT_COMPACTION_SYSTEM_PREAMBLE, CONTEXT_COMPACTION_TIMELINE_KIND, ChannelManager, CommandRegistry, ConfigManager, ContextCompactionJournalRecoveryService, ContextCompactionPreflightService, DEFAULT_AGENT_RUNTIME_ENTRY_ID, DEFAULT_SERVICE_ACTION_RISK, ExtensionManager, GatewayInboundProcessor, InboxDeliveryError, InboxDeliveryManager, LlmProviderManager, LlmUsageManager, LlmUsageStore, MAX_INBOX_DELIVERY_CONTENT_LENGTH, McpManager, McpServiceAppRuntimeService, NARP_HTTP_RUNTIME_KIND, NARP_STDIO_RUNTIME_KIND, NEXTCLAW_TIMELINE_KIND_METADATA_KEY, NcpAgentSessionJournalStore, NextclawKernel, PANEL_APP_AGENT_CAPABILITIES, PROJECT_TEMPLATE_IDS, PROVIDER_MODEL_CATALOG_REFRESH_INTERVAL_MS, PanelAppAssetTokenService, PanelAppError, PanelAppManager, PreferenceError, PreferenceManager, ProjectError, ProjectManager, ProviderManagerNcpLLMApi, ProviderModelCatalogManager, SERVICE_APP_MANIFEST_FILE_NAME, ServiceAppError, ServiceAppManager, SessionContextCompactionError, SessionContextCompactionManager, SessionManager, SessionMessageCursorError, SessionRequestManager, SessionSettingsError, SkillManager, SystemObjectReferenceError, SystemObjectReferenceManager, UpdateManifestReader, buildAgentRunSendPayload, buildContextCompactionModelProjection, buildContextCompactionTimelineNcpMessage, buildLlmUsageSummary, buildLocalizedTextMap, buildNextclawNcpRunContext, buildServiceActionId, createAgentRuntimeSessionRequestDispatcher, createAssetTools, createContextCompactionMessageId, createContextWindowSignature, createCronJobSystemObjectProvider, createInboxDeliverySystemObjectProvider, createLlmUsageRecord, describeAgentRuntimeSessionTypes, dispatchAgentRuntimeSessionRequest, dispatchChannelReplyRoute, dispatchPromptOverNcp, getAutomaticUpdateCheckDelay, getServiceActionName, getServiceAppManifestPath, getUiContentParamsBootstrapScript, getUnsignedUpdateManifest, hasLlmUsageTelemetry, injectUiContentParamsBootstrap, isAppPackageError, isContextCompactionProjectionMessage, isContextCompactionTimelineMessage, isContextWindowSnapshot, isInboxDeliveryError, isPanelAppAgentCapability, isPanelAppError, isPreferenceError, isProjectError, isReplyCapableChannel, isServiceAppError, isSessionContextCompactionError, isSessionMessageCursorError, isSessionSettingsError, isSystemObjectReferenceError, listExtensionChannelIds, listServiceAppManifestActions, mergeServiceAppRuntimeActions, normalizeAgentRuntimeSessionTypeIcon, normalizeLlmUsageModel, normalizeOptionalString, parseServiceAppManifest, parseSkillFrontmatter, readContextCompactionCheckpoint, readContextWindowEventSessionId, readLatestContextCompactionCheckpoint, readLearningLoopRuntimeConfig, readMetadataModel, readMetadataThinking, readServiceAppManifest, resolveAgentRuntimeEntries, resolveAutomaticUpdateCheckIntervalMs, resolveChannelReplyRoute, resolveEffectiveModel, resolveLegacyEventType, resolveSessionChannelContext, runGatewayInboundLoop, sanitizeLlmUsage, serializeUnsignedUpdateManifest, shouldRefreshContextWindowDuringStream, shouldRefreshContextWindowImmediately, stripSkillFrontmatter, syncSessionThinkingPreference, toNcpMessages, waitForAgentRuntimeSessionReply };
|
|
16566
|
+
export { AUTOMATIC_UPDATE_CHECK_INTERVAL_MS, AccessManager, AgentManager, AgentRunClient, AppDataError, AppDataManager, AppPackageError, AppPackageManager, AutomationManager, BuiltinNarpRuntimeProviderService, CONTEXT_COMPACTION_CONTINUATION_TEXT, CONTEXT_COMPACTION_PROJECTION_KIND, CONTEXT_COMPACTION_PROJECTION_METADATA_KEY, CONTEXT_COMPACTION_SYSTEM_PREAMBLE, CONTEXT_COMPACTION_TIMELINE_KIND, ChannelManager, CommandRegistry, ConfigManager, ContextCompactionJournalRecoveryService, ContextCompactionPreflightService, DEFAULT_AGENT_RUNTIME_ENTRY_ID, DEFAULT_SERVICE_ACTION_RISK, ExtensionManager, GatewayInboundProcessor, InboxDeliveryError, InboxDeliveryManager, LlmProviderManager, LlmUsageManager, LlmUsageStore, MAX_INBOX_DELIVERY_CONTENT_LENGTH, McpManager, McpServiceAppRuntimeService, NARP_HTTP_RUNTIME_KIND, NARP_STDIO_RUNTIME_KIND, NEXTCLAW_TIMELINE_KIND_METADATA_KEY, NcpAgentSessionJournalStore, NextclawKernel, PANEL_APP_AGENT_CAPABILITIES, PROJECT_TEMPLATE_IDS, PROVIDER_MODEL_CATALOG_REFRESH_INTERVAL_MS, PanelAppAssetTokenService, PanelAppError, PanelAppManager, PreferenceError, PreferenceManager, ProjectError, ProjectManager, ProviderManagerNcpLLMApi, ProviderModelCatalogManager, SERVICE_APP_MANIFEST_FILE_NAME, ServiceAppError, ServiceAppManager, SessionContextCompactionError, SessionContextCompactionManager, SessionManager, SessionMessageCursorError, SessionRequestManager, SessionSettingsError, SkillManager, SystemObjectReferenceError, SystemObjectReferenceManager, UpdateManifestReader, buildAgentRunSendPayload, buildContextCompactionModelProjection, buildContextCompactionTimelineNcpMessage, buildLlmUsageSummary, buildLocalizedTextMap, buildNextclawNcpRunContext, buildServiceActionId, createAgentRuntimeSessionRequestDispatcher, createAssetTools, createContextCompactionMessageId, createContextWindowSignature, createCronJobSystemObjectProvider, createInboxDeliverySystemObjectProvider, createLlmUsageRecord, describeAgentRuntimeSessionTypes, dispatchAgentRuntimeSessionRequest, dispatchChannelReplyRoute, dispatchPromptOverNcp, getAutomaticUpdateCheckDelay, getServiceActionName, getServiceAppManifestPath, getUiContentParamsBootstrapScript, getUnsignedUpdateManifest, hasLlmUsageTelemetry, injectUiContentParamsBootstrap, isAppDataError, isAppPackageError, isContextCompactionProjectionMessage, isContextCompactionTimelineMessage, isContextWindowSnapshot, isInboxDeliveryError, isPanelAppAgentCapability, isPanelAppError, isPreferenceError, isProjectError, isReplyCapableChannel, isServiceAppError, isSessionContextCompactionError, isSessionMessageCursorError, isSessionSettingsError, isSystemObjectReferenceError, listExtensionChannelIds, listServiceAppManifestActions, mergeServiceAppRuntimeActions, normalizeAgentRuntimeSessionTypeIcon, normalizeLlmUsageModel, normalizeOptionalString, parseServiceAppManifest, parseSkillFrontmatter, readContextCompactionCheckpoint, readContextWindowEventSessionId, readLatestContextCompactionCheckpoint, readLearningLoopRuntimeConfig, readMetadataModel, readMetadataThinking, readServiceAppManifest, resolveAgentRuntimeEntries, resolveAutomaticUpdateCheckIntervalMs, resolveChannelReplyRoute, resolveEffectiveModel, resolveLegacyEventType, resolveSessionChannelContext, runGatewayInboundLoop, sanitizeLlmUsage, serializeUnsignedUpdateManifest, shouldRefreshContextWindowDuringStream, shouldRefreshContextWindowImmediately, stripSkillFrontmatter, syncSessionThinkingPreference, toNcpMessages, waitForAgentRuntimeSessionReply };
|
|
16153
16567
|
|
|
16154
16568
|
//# sourceMappingURL=index.js.map
|