@nextclaw/kernel 0.6.27 → 0.7.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 +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +438 -281
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
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, AppInstanceStorageService, AppManifestService, AppRegistryService, 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;
|
|
@@ -2845,7 +2892,9 @@ var AppPackageManager = class {
|
|
|
2845
2892
|
};
|
|
2846
2893
|
listActiveComponentSources = async () => {
|
|
2847
2894
|
await this.ensureBuiltInPackages();
|
|
2848
|
-
|
|
2895
|
+
const records = await this.registryService.listApps();
|
|
2896
|
+
return (await Promise.all(records.filter((record) => record.enabled).map(async (record) => {
|
|
2897
|
+
await this.installationService.assertVersionIntegrity(record.appId, record.activeVersion);
|
|
2849
2898
|
const version = record.installedVersions[record.activeVersion];
|
|
2850
2899
|
if (!version || version.manifestSchemaVersion !== 2) return [];
|
|
2851
2900
|
return (version.components ?? []).map((component) => ({
|
|
@@ -2855,9 +2904,12 @@ var AppPackageManager = class {
|
|
|
2855
2904
|
packageVersion: record.activeVersion,
|
|
2856
2905
|
sourcePath: component.componentDirectory,
|
|
2857
2906
|
manifestPath: component.manifestPath,
|
|
2858
|
-
dataDirectory: record.dataDirectory
|
|
2907
|
+
dataDirectory: record.dataDirectory,
|
|
2908
|
+
instanceId: record.defaultInstance.id,
|
|
2909
|
+
storage: record.defaultInstance.storage,
|
|
2910
|
+
...this.resolveSecurity(version, record.appId)
|
|
2859
2911
|
}));
|
|
2860
|
-
});
|
|
2912
|
+
}))).flat();
|
|
2861
2913
|
};
|
|
2862
2914
|
listOperations = async () => await this.operationManager.list();
|
|
2863
2915
|
startOperation = async (input) => {
|
|
@@ -2873,66 +2925,115 @@ var AppPackageManager = class {
|
|
|
2873
2925
|
return await this.getPackage(result.appId);
|
|
2874
2926
|
};
|
|
2875
2927
|
enable = async (appId) => {
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2928
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
2929
|
+
const app = await this.getPackage(appId);
|
|
2930
|
+
await this.installationService.assertVersionIntegrity(appId, app.activeVersion);
|
|
2931
|
+
if (app.enabled) return app;
|
|
2932
|
+
await this.assertEngineCompatibility(appId);
|
|
2933
|
+
const sources = this.toComponentSources(app);
|
|
2934
|
+
await this.runtimeHooks.assertCanActivate(sources);
|
|
2935
|
+
await this.installationService.setEnabled(appId, true);
|
|
2936
|
+
return await this.getPackage(appId);
|
|
2937
|
+
});
|
|
2883
2938
|
};
|
|
2884
2939
|
disable = async (appId) => {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2940
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
2941
|
+
const app = await this.getPackage(appId);
|
|
2942
|
+
if (!app.enabled) return app;
|
|
2943
|
+
await this.runtimeHooks.beforeDeactivate(this.toComponentSources(app));
|
|
2944
|
+
await this.installationService.setEnabled(appId, false);
|
|
2945
|
+
return await this.getPackage(appId);
|
|
2946
|
+
});
|
|
2890
2947
|
};
|
|
2891
2948
|
update = async (appId, options = {}) => {
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
if (updated
|
|
2899
|
-
|
|
2900
|
-
package: updated,
|
|
2949
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
2950
|
+
const current = await this.getPackage(appId);
|
|
2951
|
+
const result = await this.installationService.update(appId, {
|
|
2952
|
+
...options,
|
|
2953
|
+
activate: false
|
|
2954
|
+
});
|
|
2955
|
+
if (!result.updated) return {
|
|
2956
|
+
package: current,
|
|
2901
2957
|
result
|
|
2902
2958
|
};
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2959
|
+
let activated = false;
|
|
2960
|
+
let deactivated = false;
|
|
2961
|
+
try {
|
|
2962
|
+
await this.assertEngineCompatibility(appId, result.version);
|
|
2963
|
+
const candidate = await this.toPackageView(await this.installationService.info(appId), result.version);
|
|
2964
|
+
if (current.enabled) {
|
|
2965
|
+
await this.runtimeHooks.beforeDeactivate(this.toComponentSources(current));
|
|
2966
|
+
deactivated = true;
|
|
2967
|
+
await this.runtimeHooks.assertCanActivate(this.toComponentSources(candidate));
|
|
2968
|
+
}
|
|
2969
|
+
activated = (await this.installationService.rollback(appId, result.version)).rolledBack;
|
|
2970
|
+
return {
|
|
2971
|
+
package: await this.getPackage(appId),
|
|
2972
|
+
result
|
|
2973
|
+
};
|
|
2974
|
+
} catch (error) {
|
|
2975
|
+
if (activated) await this.installationService.rollback(appId, result.previousVersion);
|
|
2976
|
+
if (deactivated) try {
|
|
2977
|
+
await this.runtimeHooks.assertCanActivate(this.toComponentSources(current));
|
|
2978
|
+
} catch (recoveryError) {
|
|
2979
|
+
throw new AggregateError([error, recoveryError], `应用 ${appId} 更新失败,且旧 runtime 恢复探测失败。`);
|
|
2980
|
+
}
|
|
2981
|
+
throw error;
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2907
2984
|
};
|
|
2908
2985
|
rollback = async (appId, version) => {
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2986
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
2987
|
+
const current = await this.getPackage(appId);
|
|
2988
|
+
if (current.activeVersion === version) return {
|
|
2989
|
+
package: current,
|
|
2990
|
+
result: {
|
|
2991
|
+
appId,
|
|
2992
|
+
activeVersion: version,
|
|
2993
|
+
previousVersion: version,
|
|
2994
|
+
enabled: current.enabled,
|
|
2995
|
+
rolledBack: false
|
|
2996
|
+
}
|
|
2919
2997
|
};
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2998
|
+
await this.assertEngineCompatibility(appId, version);
|
|
2999
|
+
const candidate = await this.toPackageView(await this.installationService.info(appId), version);
|
|
3000
|
+
let deactivated = false;
|
|
3001
|
+
let result;
|
|
3002
|
+
try {
|
|
3003
|
+
if (current.enabled) {
|
|
3004
|
+
await this.runtimeHooks.beforeDeactivate(this.toComponentSources(current));
|
|
3005
|
+
deactivated = true;
|
|
3006
|
+
await this.runtimeHooks.assertCanActivate(this.toComponentSources(candidate));
|
|
3007
|
+
}
|
|
3008
|
+
result = await this.installationService.rollback(appId, version);
|
|
3009
|
+
return {
|
|
3010
|
+
package: await this.getPackage(appId),
|
|
3011
|
+
result
|
|
3012
|
+
};
|
|
3013
|
+
} catch (error) {
|
|
3014
|
+
if (result?.rolledBack) await this.installationService.rollback(appId, result.previousVersion);
|
|
3015
|
+
if (deactivated) try {
|
|
3016
|
+
await this.runtimeHooks.assertCanActivate(this.toComponentSources(current));
|
|
3017
|
+
} catch (recoveryError) {
|
|
3018
|
+
throw new AggregateError([error, recoveryError], `应用 ${appId} 回滚失败,且旧 runtime 恢复探测失败。`);
|
|
3019
|
+
}
|
|
3020
|
+
throw error;
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
2924
3023
|
};
|
|
2925
3024
|
uninstall = async (appId, purgeData) => {
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3025
|
+
return await this.installationService.withAppOperation(appId, async () => {
|
|
3026
|
+
const current = await this.getPackage(appId);
|
|
3027
|
+
if (current.builtIn) await this.registryService.setBuiltInSuppressed(appId, true);
|
|
3028
|
+
try {
|
|
3029
|
+
if (current.enabled) await this.runtimeHooks.beforeDeactivate(this.toComponentSources(current));
|
|
3030
|
+
await this.runtimeHooks.beforeUninstall(this.toComponentSources(current));
|
|
3031
|
+
return await this.installationService.uninstall(appId, purgeData);
|
|
3032
|
+
} catch (error) {
|
|
3033
|
+
if (current.builtIn) await this.registryService.setBuiltInSuppressed(appId, false);
|
|
3034
|
+
throw error;
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
2936
3037
|
};
|
|
2937
3038
|
ensureBuiltInPackages = async () => {
|
|
2938
3039
|
this.builtInBootstrapPromise ??= this.installBuiltInPackages();
|
|
@@ -2944,13 +3045,22 @@ var AppPackageManager = class {
|
|
|
2944
3045
|
if (!isAppComponentManifestBundle(manifest)) continue;
|
|
2945
3046
|
if (await this.registryService.isBuiltInSuppressed(manifest.manifest.id)) continue;
|
|
2946
3047
|
if ((await this.registryService.getApp(manifest.manifest.id))?.installedVersions[manifest.manifest.version]) continue;
|
|
2947
|
-
await this.installationService.install(appDirectory
|
|
3048
|
+
await this.installationService.install(appDirectory, { trustedPublisher: {
|
|
3049
|
+
id: "nextclaw",
|
|
3050
|
+
name: "NextClaw",
|
|
3051
|
+
url: "https://nextclaw.io"
|
|
3052
|
+
} });
|
|
2948
3053
|
}
|
|
2949
3054
|
};
|
|
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.
|
|
3055
|
+
toPackageView = async (info, selectedVersion = info.activeVersion) => {
|
|
3056
|
+
const activeVersion = info.installedVersions.find((version) => version.version === selectedVersion);
|
|
3057
|
+
if (!activeVersion) throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `应用 ${info.appId} 缺少版本 ${selectedVersion}。`);
|
|
3058
|
+
const packagePresentation = await this.presentationService.readManifest(path.join(activeVersion.installDirectory, "manifest.json"));
|
|
3059
|
+
const manifestBundle = await this.manifestService.load(activeVersion.installDirectory);
|
|
3060
|
+
const security = manifestBundle.manifest.schemaVersion === 2 ? this.manifestService.resolvePlatformSecurity(manifestBundle.manifest) : {
|
|
3061
|
+
runtimeProfile: "wasi",
|
|
3062
|
+
isolation: manifestBundle.manifest.main.kind === "wasi-http-component" ? "host-mediated" : "sandboxed"
|
|
3063
|
+
};
|
|
2954
3064
|
return {
|
|
2955
3065
|
id: info.appId,
|
|
2956
3066
|
name: info.name,
|
|
@@ -2958,7 +3068,7 @@ var AppPackageManager = class {
|
|
|
2958
3068
|
icon: packagePresentation.icon,
|
|
2959
3069
|
nameI18n: packagePresentation.nameI18n,
|
|
2960
3070
|
descriptionI18n: packagePresentation.descriptionI18n,
|
|
2961
|
-
activeVersion:
|
|
3071
|
+
activeVersion: selectedVersion,
|
|
2962
3072
|
installedVersions: info.installedVersions.map((version) => version.version),
|
|
2963
3073
|
enabled: info.enabled,
|
|
2964
3074
|
builtIn: await this.isBuiltInAppId(info.appId),
|
|
@@ -2967,43 +3077,50 @@ var AppPackageManager = class {
|
|
|
2967
3077
|
kind: component.kind,
|
|
2968
3078
|
id: component.id,
|
|
2969
3079
|
packageId: info.appId,
|
|
2970
|
-
packageVersion:
|
|
3080
|
+
packageVersion: selectedVersion,
|
|
2971
3081
|
sourcePath: component.componentDirectory,
|
|
2972
3082
|
manifestPath: component.manifestPath,
|
|
2973
3083
|
dataDirectory: info.dataDirectory,
|
|
2974
|
-
|
|
3084
|
+
instanceId: info.instance.id,
|
|
3085
|
+
storage: info.storage,
|
|
3086
|
+
runtimeProfile: security.runtimeProfile,
|
|
3087
|
+
isolation: security.isolation,
|
|
3088
|
+
...await this.presentationService.readManifest(component.manifestPath)
|
|
2975
3089
|
}))),
|
|
2976
|
-
dataDirectory: info.dataDirectory
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3090
|
+
dataDirectory: info.dataDirectory,
|
|
3091
|
+
instanceId: info.instance.id,
|
|
3092
|
+
storage: info.storage,
|
|
3093
|
+
storageUsage: info.storageUsage,
|
|
3094
|
+
runtimeProfile: security.runtimeProfile,
|
|
3095
|
+
isolation: security.isolation
|
|
3096
|
+
};
|
|
3097
|
+
};
|
|
3098
|
+
resolveSecurity = (version, appId) => {
|
|
3099
|
+
if (version.security) return {
|
|
3100
|
+
runtimeProfile: version.security.runtimeProfile,
|
|
3101
|
+
isolation: version.security.isolation
|
|
3102
|
+
};
|
|
3103
|
+
const hasService = version.components?.some((component) => component.kind === "service") ?? false;
|
|
3104
|
+
if (version.manifestSchemaVersion !== 2) throw new AppPackageError("APP_PACKAGE_INCOMPATIBLE", `应用 ${appId} 仍使用 legacy schema,不能投影组件。`);
|
|
3105
|
+
return hasService ? {
|
|
3106
|
+
runtimeProfile: "native-process",
|
|
3107
|
+
isolation: "full-user"
|
|
3108
|
+
} : {
|
|
3109
|
+
runtimeProfile: "panel-only",
|
|
3110
|
+
isolation: "sandboxed"
|
|
2989
3111
|
};
|
|
2990
3112
|
};
|
|
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
|
-
};
|
|
2997
3113
|
toComponentSources = (app) => app.components.map((component) => ({ ...component }));
|
|
2998
|
-
assertEngineCompatibility = async (appId) => {
|
|
3114
|
+
assertEngineCompatibility = async (appId, selectedVersion) => {
|
|
2999
3115
|
const productVersion = this.params.productVersion?.trim();
|
|
3000
3116
|
if (!productVersion) return;
|
|
3001
3117
|
const info = await this.installationService.info(appId);
|
|
3002
|
-
const
|
|
3003
|
-
|
|
3118
|
+
const targetVersion = selectedVersion ?? info.activeVersion;
|
|
3119
|
+
const activeVersion = info.installedVersions.find((version) => version.version === targetVersion);
|
|
3120
|
+
if (!activeVersion) throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `应用 ${appId} 缺少版本 ${targetVersion}。`);
|
|
3004
3121
|
const manifestBundle = await this.manifestService.load(activeVersion.installDirectory);
|
|
3005
3122
|
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}@${
|
|
3123
|
+
if (engineRange && !satisfiesAppEngineVersion(productVersion, engineRange)) throw new AppPackageError("APP_PACKAGE_INCOMPATIBLE", `应用 ${appId}@${targetVersion} 要求 NextClaw ${engineRange},当前版本为 ${productVersion}。`);
|
|
3007
3124
|
};
|
|
3008
3125
|
listBuiltInDefinitions = async () => {
|
|
3009
3126
|
if (!this.params.builtInAppsDirectory) return [];
|
|
@@ -3027,29 +3144,6 @@ var AppPackageManager = class {
|
|
|
3027
3144
|
return await this.builtInDefinitionsPromise;
|
|
3028
3145
|
};
|
|
3029
3146
|
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
3147
|
executeOperation = async (input, report) => {
|
|
3054
3148
|
if (input.action === "install") {
|
|
3055
3149
|
const installed = await this.install(input.source, input.registryUrl, async (phase) => await report(phase));
|
|
@@ -7107,7 +7201,7 @@ var SessionManager = class {
|
|
|
7107
7201
|
limit,
|
|
7108
7202
|
...cursor ? { cursor } : {}
|
|
7109
7203
|
});
|
|
7110
|
-
if (!page) return page;
|
|
7204
|
+
if (!page || cursor) return page;
|
|
7111
7205
|
const record = await this.options.journalStore.getSession(normalizedSessionId);
|
|
7112
7206
|
if (!record) return page;
|
|
7113
7207
|
const contextWindow = (await this.createSummaryWithContextWindow(record)).contextWindow ?? null;
|
|
@@ -9731,7 +9825,7 @@ var McpServiceAppRuntimeService = class {
|
|
|
9731
9825
|
toMcpServerRecord = (app, manifest) => ({
|
|
9732
9826
|
name: app.id,
|
|
9733
9827
|
definition: {
|
|
9734
|
-
enabled:
|
|
9828
|
+
enabled: app.enabled,
|
|
9735
9829
|
transport: {
|
|
9736
9830
|
type: "stdio",
|
|
9737
9831
|
command: manifest.command,
|
|
@@ -9752,7 +9846,16 @@ var McpServiceAppRuntimeService = class {
|
|
|
9752
9846
|
});
|
|
9753
9847
|
createAppRuntimeEnv = (app) => {
|
|
9754
9848
|
const env = {};
|
|
9755
|
-
if (app.
|
|
9849
|
+
if (app.storage) {
|
|
9850
|
+
env.NEXTCLAW_APP_INSTANCE_ID = app.storage.instanceId;
|
|
9851
|
+
env.NEXTCLAW_APP_COMPONENT_ID = app.id;
|
|
9852
|
+
env.NEXTCLAW_APP_DATA_DIR = app.storage.dataDirectory;
|
|
9853
|
+
env.NEXTCLAW_APP_CONFIG_DIR = app.storage.configDirectory;
|
|
9854
|
+
env.NEXTCLAW_APP_STATE_DIR = app.storage.stateDirectory;
|
|
9855
|
+
env.NEXTCLAW_APP_CACHE_DIR = app.storage.cacheDirectory;
|
|
9856
|
+
env.NEXTCLAW_APP_TMP_DIR = app.storage.temporaryDirectory;
|
|
9857
|
+
env.NEXTCLAW_APP_LOG_DIR = app.storage.logsDirectory;
|
|
9858
|
+
} else if (app.dataDirectory) env.NEXTCLAW_APP_DATA_DIR = app.dataDirectory;
|
|
9756
9859
|
if (app.sourceKind !== "package" || !app.packageId || !app.packageVersion || !app.packageDirectory) return env;
|
|
9757
9860
|
env.NEXTCLAW_APP_ID = app.packageId;
|
|
9758
9861
|
env.NEXTCLAW_APP_VERSION = app.packageVersion;
|
|
@@ -9775,6 +9878,182 @@ var McpServiceAppRuntimeService = class {
|
|
|
9775
9878
|
};
|
|
9776
9879
|
};
|
|
9777
9880
|
//#endregion
|
|
9881
|
+
//#region src/utils/service-app-manifest.utils.ts
|
|
9882
|
+
const SERVICE_APP_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9883
|
+
const SERVICE_ACTION_RISKS = new Set([
|
|
9884
|
+
"read",
|
|
9885
|
+
"write",
|
|
9886
|
+
"external",
|
|
9887
|
+
"dangerous"
|
|
9888
|
+
]);
|
|
9889
|
+
const SERVICE_APP_MANIFEST_FILE_NAME = "service-app.json";
|
|
9890
|
+
function getServiceAppManifestPath(dirPath) {
|
|
9891
|
+
return join(dirPath, SERVICE_APP_MANIFEST_FILE_NAME);
|
|
9892
|
+
}
|
|
9893
|
+
async function readServiceAppManifest(dirPath) {
|
|
9894
|
+
return parseServiceAppManifest(await readFile(getServiceAppManifestPath(dirPath), "utf8"));
|
|
9895
|
+
}
|
|
9896
|
+
function parseServiceAppManifest(raw) {
|
|
9897
|
+
let parsed;
|
|
9898
|
+
try {
|
|
9899
|
+
parsed = JSON.parse(raw);
|
|
9900
|
+
} catch (error) {
|
|
9901
|
+
throw new Error(`service-app.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
9902
|
+
}
|
|
9903
|
+
if (!isRecord$9(parsed)) throw new Error("service-app.json must contain an object.");
|
|
9904
|
+
const id = readRequiredString$6(parsed, "id");
|
|
9905
|
+
if (!SERVICE_APP_ID_PATTERN.test(id)) throw new Error("service app id must be kebab-case.");
|
|
9906
|
+
const protocol = readOptionalString$7(parsed, "protocol") ?? "mcp";
|
|
9907
|
+
if (protocol !== "mcp") throw new Error("service app protocol must be mcp.");
|
|
9908
|
+
return {
|
|
9909
|
+
id,
|
|
9910
|
+
title: readRequiredString$6(parsed, "title"),
|
|
9911
|
+
description: readOptionalString$7(parsed, "description"),
|
|
9912
|
+
enabled: readOptionalBoolean$1(parsed, "enabled") ?? true,
|
|
9913
|
+
protocol,
|
|
9914
|
+
command: readRequiredString$6(parsed, "command"),
|
|
9915
|
+
args: readStringArray(parsed.args, "args"),
|
|
9916
|
+
actions: readManifestActions(parsed.actions)
|
|
9917
|
+
};
|
|
9918
|
+
}
|
|
9919
|
+
function readManifestActions(value) {
|
|
9920
|
+
if (value === void 0) throw new Error("service app actions are required.");
|
|
9921
|
+
if (!isRecord$9(value)) throw new Error("service app actions must be an object.");
|
|
9922
|
+
if (Object.keys(value).length === 0) throw new Error("service app actions cannot be empty.");
|
|
9923
|
+
const actions = {};
|
|
9924
|
+
for (const [name, action] of Object.entries(value)) {
|
|
9925
|
+
if (!name.trim()) throw new Error("service app action name cannot be empty.");
|
|
9926
|
+
if (!isRecord$9(action)) throw new Error(`service app action ${name} must be an object.`);
|
|
9927
|
+
const risk = readOptionalString$7(action, "risk");
|
|
9928
|
+
if (risk !== void 0 && !SERVICE_ACTION_RISKS.has(risk)) throw new Error(`service app action ${name} has invalid risk.`);
|
|
9929
|
+
const inputSchema = action.inputSchema;
|
|
9930
|
+
if (inputSchema !== void 0 && !isRecord$9(inputSchema)) throw new Error(`service app action ${name} inputSchema must be an object.`);
|
|
9931
|
+
actions[name] = {
|
|
9932
|
+
risk,
|
|
9933
|
+
title: readOptionalString$7(action, "title"),
|
|
9934
|
+
description: readOptionalString$7(action, "description"),
|
|
9935
|
+
inputSchema
|
|
9936
|
+
};
|
|
9937
|
+
}
|
|
9938
|
+
return actions;
|
|
9939
|
+
}
|
|
9940
|
+
function readRequiredString$6(record, key) {
|
|
9941
|
+
const value = readOptionalString$7(record, key);
|
|
9942
|
+
if (!value) throw new Error(`service app ${key} is required.`);
|
|
9943
|
+
return value;
|
|
9944
|
+
}
|
|
9945
|
+
function readOptionalString$7(record, key) {
|
|
9946
|
+
return typeof record[key] === "string" && record[key].trim() ? record[key].trim() : void 0;
|
|
9947
|
+
}
|
|
9948
|
+
function readOptionalBoolean$1(record, key) {
|
|
9949
|
+
if (record[key] === void 0) return;
|
|
9950
|
+
if (typeof record[key] !== "boolean") throw new Error(`service app ${key} must be boolean.`);
|
|
9951
|
+
return record[key];
|
|
9952
|
+
}
|
|
9953
|
+
function readStringArray(value, key) {
|
|
9954
|
+
if (value === void 0) return [];
|
|
9955
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(`service app ${key} must be a string array.`);
|
|
9956
|
+
return value;
|
|
9957
|
+
}
|
|
9958
|
+
function isRecord$9(value) {
|
|
9959
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9960
|
+
}
|
|
9961
|
+
//#endregion
|
|
9962
|
+
//#region src/services/service-app-record.service.ts
|
|
9963
|
+
var ServiceAppRecordService = class {
|
|
9964
|
+
instanceStorageService = new AppInstanceStorageService();
|
|
9965
|
+
constructor(params) {
|
|
9966
|
+
this.params = params;
|
|
9967
|
+
}
|
|
9968
|
+
buildWorkspaceRecord = async (serviceAppsPath, dirName) => {
|
|
9969
|
+
const dirPath = join(serviceAppsPath, dirName);
|
|
9970
|
+
try {
|
|
9971
|
+
const manifest = await readServiceAppManifest(dirPath);
|
|
9972
|
+
return this.fromManifest(dirPath, manifest, void 0, await this.materializeWorkspaceStorage(manifest.id));
|
|
9973
|
+
} catch (error) {
|
|
9974
|
+
if (this.isMissingFileError(error)) return null;
|
|
9975
|
+
return this.failedWorkspaceRecord(dirName, dirPath, error);
|
|
9976
|
+
}
|
|
9977
|
+
};
|
|
9978
|
+
buildPackageRecord = async (source) => {
|
|
9979
|
+
try {
|
|
9980
|
+
const manifest = await readServiceAppManifest(source.sourcePath);
|
|
9981
|
+
if (manifest.id !== source.id) throw new Error(`service component id mismatch: ${source.id}`);
|
|
9982
|
+
return this.fromManifest(source.sourcePath, manifest, source, source.storage);
|
|
9983
|
+
} catch (error) {
|
|
9984
|
+
return this.failedPackageRecord(source, error);
|
|
9985
|
+
}
|
|
9986
|
+
};
|
|
9987
|
+
materializeWorkspaceStorage = async (serviceId) => {
|
|
9988
|
+
const instanceDirectory = join(this.params.getWorkspacePath(), ".nextclaw", "app-instances", serviceId, "default");
|
|
9989
|
+
return (await this.instanceStorageService.materialize({
|
|
9990
|
+
appId: serviceId,
|
|
9991
|
+
instanceId: "default",
|
|
9992
|
+
instanceDirectory
|
|
9993
|
+
})).storage;
|
|
9994
|
+
};
|
|
9995
|
+
fromManifest = (dirPath, manifest, packageSource, storage) => {
|
|
9996
|
+
const runtimeStatus = this.params.runtimeService.getStatus(manifest.id);
|
|
9997
|
+
return {
|
|
9998
|
+
id: manifest.id,
|
|
9999
|
+
title: manifest.title,
|
|
10000
|
+
description: manifest.description,
|
|
10001
|
+
dirPath,
|
|
10002
|
+
manifestPath: getServiceAppManifestPath(dirPath),
|
|
10003
|
+
command: manifest.command,
|
|
10004
|
+
args: manifest.args,
|
|
10005
|
+
cwd: dirPath,
|
|
10006
|
+
enabled: packageSource ? true : manifest.enabled,
|
|
10007
|
+
protocol: manifest.protocol,
|
|
10008
|
+
status: packageSource || manifest.enabled ? runtimeStatus.status : "stopped",
|
|
10009
|
+
lastError: runtimeStatus.lastError,
|
|
10010
|
+
lastStartedAt: runtimeStatus.lastStartedAt,
|
|
10011
|
+
lastReadyAt: runtimeStatus.lastReadyAt,
|
|
10012
|
+
lastFailedAt: runtimeStatus.lastFailedAt,
|
|
10013
|
+
sourceKind: packageSource ? "package" : "workspace",
|
|
10014
|
+
packageId: packageSource?.packageId,
|
|
10015
|
+
packageVersion: packageSource?.packageVersion,
|
|
10016
|
+
packageDirectory: packageSource ? join(packageSource.sourcePath, "..", "..") : void 0,
|
|
10017
|
+
dataDirectory: storage?.dataDirectory,
|
|
10018
|
+
instanceId: packageSource?.instanceId ?? storage?.instanceId,
|
|
10019
|
+
storage,
|
|
10020
|
+
isolation: packageSource?.isolation ?? "full-user"
|
|
10021
|
+
};
|
|
10022
|
+
};
|
|
10023
|
+
failedWorkspaceRecord = (id, dirPath, error) => ({
|
|
10024
|
+
id,
|
|
10025
|
+
title: this.toTitle(id),
|
|
10026
|
+
dirPath,
|
|
10027
|
+
manifestPath: getServiceAppManifestPath(dirPath),
|
|
10028
|
+
cwd: dirPath,
|
|
10029
|
+
enabled: false,
|
|
10030
|
+
protocol: "mcp",
|
|
10031
|
+
status: "failed",
|
|
10032
|
+
lastError: error instanceof Error ? error.message : String(error)
|
|
10033
|
+
});
|
|
10034
|
+
failedPackageRecord = (source, error) => ({
|
|
10035
|
+
id: source.id,
|
|
10036
|
+
title: this.toTitle(source.id),
|
|
10037
|
+
dirPath: source.sourcePath,
|
|
10038
|
+
manifestPath: source.manifestPath,
|
|
10039
|
+
cwd: source.sourcePath,
|
|
10040
|
+
enabled: false,
|
|
10041
|
+
protocol: "mcp",
|
|
10042
|
+
status: "failed",
|
|
10043
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
10044
|
+
sourceKind: "package",
|
|
10045
|
+
packageId: source.packageId,
|
|
10046
|
+
packageVersion: source.packageVersion,
|
|
10047
|
+
packageDirectory: join(source.sourcePath, "..", ".."),
|
|
10048
|
+
dataDirectory: source.dataDirectory,
|
|
10049
|
+
instanceId: source.instanceId,
|
|
10050
|
+
storage: source.storage,
|
|
10051
|
+
isolation: source.isolation
|
|
10052
|
+
});
|
|
10053
|
+
toTitle = (value) => basename(value).replace(/[-_]+/g, " ").trim() || value;
|
|
10054
|
+
isMissingFileError = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
10055
|
+
};
|
|
10056
|
+
//#endregion
|
|
9778
10057
|
//#region src/stores/service-action-grant.store.ts
|
|
9779
10058
|
const EMPTY_GRANTS = {
|
|
9780
10059
|
version: 1,
|
|
@@ -9784,9 +10063,9 @@ var ServiceActionGrantStore = class {
|
|
|
9784
10063
|
constructor(storePath) {
|
|
9785
10064
|
this.storePath = storePath;
|
|
9786
10065
|
}
|
|
9787
|
-
isGranted = async (caller, actionId) => {
|
|
9788
|
-
const
|
|
9789
|
-
return Boolean(
|
|
10066
|
+
isGranted = async (caller, actionId, currentRisk) => {
|
|
10067
|
+
const grant = (await this.load()).grants[getServiceActionCallerKey(caller)]?.actions[actionId];
|
|
10068
|
+
return Boolean(grant && (currentRisk === void 0 || grant.risk === currentRisk));
|
|
9790
10069
|
};
|
|
9791
10070
|
grant = async ({ actionId, caller, grantedAt, risk }) => {
|
|
9792
10071
|
const data = await this.load();
|
|
@@ -9853,12 +10132,12 @@ var ServiceActionGrantStore = class {
|
|
|
9853
10132
|
};
|
|
9854
10133
|
};
|
|
9855
10134
|
function normalizeStoreData(value) {
|
|
9856
|
-
if (!isRecord$
|
|
10135
|
+
if (!isRecord$8(value) || value.version !== 1 || !isRecord$8(value.grants)) return structuredClone(EMPTY_GRANTS);
|
|
9857
10136
|
const grants = {};
|
|
9858
10137
|
for (const [callerKey, callerValue] of Object.entries(value.grants)) {
|
|
9859
|
-
if (!isRecord$
|
|
10138
|
+
if (!isRecord$8(callerValue) || !isRecord$8(callerValue.actions)) continue;
|
|
9860
10139
|
const actions = {};
|
|
9861
|
-
for (const [actionId, actionValue] of Object.entries(callerValue.actions)) if (isRecord$
|
|
10140
|
+
for (const [actionId, actionValue] of Object.entries(callerValue.actions)) if (isRecord$8(actionValue) && typeof actionValue.grantedAt === "string" && isServiceActionRisk(actionValue.risk)) actions[actionId] = {
|
|
9862
10141
|
grantedAt: actionValue.grantedAt,
|
|
9863
10142
|
risk: actionValue.risk
|
|
9864
10143
|
};
|
|
@@ -9872,92 +10151,11 @@ function normalizeStoreData(value) {
|
|
|
9872
10151
|
function isServiceActionRisk(value) {
|
|
9873
10152
|
return value === "read" || value === "write" || value === "external" || value === "dangerous";
|
|
9874
10153
|
}
|
|
9875
|
-
function isRecord$
|
|
10154
|
+
function isRecord$8(value) {
|
|
9876
10155
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9877
10156
|
}
|
|
9878
10157
|
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);
|
|
10158
|
+
return isRecord$8(error) && error.code === "ENOENT";
|
|
9961
10159
|
}
|
|
9962
10160
|
//#endregion
|
|
9963
10161
|
//#region src/utils/service-app-runtime-action.utils.ts
|
|
@@ -10006,17 +10204,22 @@ function isServiceAppError(error) {
|
|
|
10006
10204
|
}
|
|
10007
10205
|
var ServiceAppManager = class {
|
|
10008
10206
|
runtimeService;
|
|
10207
|
+
recordService;
|
|
10009
10208
|
constructor(params) {
|
|
10010
10209
|
this.params = params;
|
|
10011
10210
|
this.runtimeService = params.runtimeService ?? new McpServiceAppRuntimeService({ getConfig: () => params.configManager.config });
|
|
10211
|
+
this.recordService = new ServiceAppRecordService({
|
|
10212
|
+
getWorkspacePath: this.getWorkspacePath,
|
|
10213
|
+
runtimeService: this.runtimeService
|
|
10214
|
+
});
|
|
10012
10215
|
}
|
|
10013
10216
|
listServiceApps = async () => {
|
|
10014
10217
|
const workspacePath = this.getWorkspacePath();
|
|
10015
10218
|
const serviceAppsPath = this.getServiceAppsPath(workspacePath);
|
|
10016
10219
|
const dirNames = await this.listServiceAppDirNames(serviceAppsPath);
|
|
10017
|
-
const workspaceEntries = await Promise.all(dirNames.map((dirName) => this.
|
|
10220
|
+
const workspaceEntries = await Promise.all(dirNames.map((dirName) => this.recordService.buildWorkspaceRecord(serviceAppsPath, dirName)));
|
|
10018
10221
|
const packageSources = (await this.listPackageComponentSources()).filter((component) => component.kind === "service");
|
|
10019
|
-
const packageEntries = await Promise.all(packageSources.map((source) => this.
|
|
10222
|
+
const packageEntries = await Promise.all(packageSources.map((source) => this.recordService.buildPackageRecord(source)));
|
|
10020
10223
|
return {
|
|
10021
10224
|
workspacePath,
|
|
10022
10225
|
serviceAppsPath,
|
|
@@ -10048,7 +10251,8 @@ var ServiceAppManager = class {
|
|
|
10048
10251
|
const { manifest, record } = await this.requireServiceAppForAction(actionId);
|
|
10049
10252
|
const actionName = getServiceActionName(actionId, record.id);
|
|
10050
10253
|
if (!Object.hasOwn(manifest.actions, actionName)) throw new ServiceAppError("SERVICE_APP_ACTION_NOT_FOUND", "service action not found");
|
|
10051
|
-
|
|
10254
|
+
const declaredRisk = manifest.actions[actionName]?.risk ?? "dangerous";
|
|
10255
|
+
if (!await this.createGrantStore().isGranted(request.caller, actionId, declaredRisk)) throw new ServiceAppError("AUTHORIZATION_REQUIRED", `This panel app needs permission to call ${actionId}.`);
|
|
10052
10256
|
return {
|
|
10053
10257
|
actionId,
|
|
10054
10258
|
result: await this.runtimeService.invokeAction({
|
|
@@ -10125,6 +10329,31 @@ var ServiceAppManager = class {
|
|
|
10125
10329
|
const conflictsWithPackage = activePackageSources.some((active) => active.kind === "service" && active.id === component.id && active.packageId !== component.packageId);
|
|
10126
10330
|
if (workspaceIds.has(component.id) || conflictsWithPackage) throw new AppPackageError("APP_PACKAGE_CONFLICT", `Service component id 冲突:${component.id}`);
|
|
10127
10331
|
}
|
|
10332
|
+
for (const component of serviceComponents) {
|
|
10333
|
+
let manifest;
|
|
10334
|
+
try {
|
|
10335
|
+
manifest = await readServiceAppManifest(component.sourcePath);
|
|
10336
|
+
} catch (error) {
|
|
10337
|
+
throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `Service component ${component.id} manifest 无效:${error instanceof Error ? error.message : String(error)}`);
|
|
10338
|
+
}
|
|
10339
|
+
try {
|
|
10340
|
+
const record = this.recordService.fromManifest(component.sourcePath, manifest, component, component.storage);
|
|
10341
|
+
const runtimeActions = await this.runtimeService.listActions({
|
|
10342
|
+
app: record,
|
|
10343
|
+
manifest
|
|
10344
|
+
});
|
|
10345
|
+
const runtimeStatus = this.runtimeService.getStatus(component.id);
|
|
10346
|
+
if (runtimeStatus.status === "failed") throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `Service component ${component.id} 启动探测失败:${runtimeStatus.lastError ?? "unknown error"}`);
|
|
10347
|
+
const invalidAction = mergeServiceAppRuntimeActions({
|
|
10348
|
+
record,
|
|
10349
|
+
manifest,
|
|
10350
|
+
runtimeActions
|
|
10351
|
+
}).find((action) => action.runtimeState === "missing" || action.runtimeState === "undeclared");
|
|
10352
|
+
if (invalidAction) throw new AppPackageError("APP_PACKAGE_OPERATION_FAILED", `Service component ${component.id} action 合同不一致:${invalidAction.id} (${invalidAction.runtimeState})`);
|
|
10353
|
+
} finally {
|
|
10354
|
+
await this.runtimeService.restart(component.id);
|
|
10355
|
+
}
|
|
10356
|
+
}
|
|
10128
10357
|
};
|
|
10129
10358
|
deactivatePackageComponents = async (components) => {
|
|
10130
10359
|
const serviceIds = components.filter((component) => component.kind === "service").map((component) => component.id);
|
|
@@ -10137,7 +10366,7 @@ var ServiceAppManager = class {
|
|
|
10137
10366
|
};
|
|
10138
10367
|
withGrantState = async (action, params) => {
|
|
10139
10368
|
if (!params.caller) return action;
|
|
10140
|
-
const granted = await this.createGrantStore().isGranted(params.caller, action.id);
|
|
10369
|
+
const granted = await this.createGrantStore().isGranted(params.caller, action.id, action.risk);
|
|
10141
10370
|
return {
|
|
10142
10371
|
...action,
|
|
10143
10372
|
grantState: resolveServiceActionGrantState({
|
|
@@ -10175,7 +10404,7 @@ var ServiceAppManager = class {
|
|
|
10175
10404
|
if (manifest.id !== appId) throw new ServiceAppError("SERVICE_APP_INVALID_MANIFEST", "service app manifest id must match directory name");
|
|
10176
10405
|
return {
|
|
10177
10406
|
manifest,
|
|
10178
|
-
record: this.
|
|
10407
|
+
record: this.recordService.fromManifest(dirPath, manifest, packageSource, packageSource?.storage ?? await this.recordService.materializeWorkspaceStorage(manifest.id))
|
|
10179
10408
|
};
|
|
10180
10409
|
} catch (error) {
|
|
10181
10410
|
if (isServiceAppError(error)) throw error;
|
|
@@ -10193,7 +10422,7 @@ var ServiceAppManager = class {
|
|
|
10193
10422
|
const manifest = await readServiceAppManifest(dirPath);
|
|
10194
10423
|
return {
|
|
10195
10424
|
manifest,
|
|
10196
|
-
record: this.
|
|
10425
|
+
record: this.recordService.fromManifest(dirPath, manifest, void 0, await this.recordService.materializeWorkspaceStorage(manifest.id))
|
|
10197
10426
|
};
|
|
10198
10427
|
} catch {
|
|
10199
10428
|
return null;
|
|
@@ -10204,7 +10433,7 @@ var ServiceAppManager = class {
|
|
|
10204
10433
|
const manifest = await readServiceAppManifest(component.sourcePath);
|
|
10205
10434
|
return {
|
|
10206
10435
|
manifest,
|
|
10207
|
-
record: this.
|
|
10436
|
+
record: this.recordService.fromManifest(component.sourcePath, manifest, component, component.storage)
|
|
10208
10437
|
};
|
|
10209
10438
|
} catch {
|
|
10210
10439
|
return null;
|
|
@@ -10212,51 +10441,6 @@ var ServiceAppManager = class {
|
|
|
10212
10441
|
}));
|
|
10213
10442
|
return [...workspaceEntries, ...packageEntries].filter((entry) => Boolean(entry));
|
|
10214
10443
|
};
|
|
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
10444
|
assertCaller = (caller) => {
|
|
10261
10445
|
if (caller.surface !== "panel-app" || !caller.appId.trim()) throw new ServiceAppError("SERVICE_APP_INVALID_CALLER", "service action caller is invalid");
|
|
10262
10446
|
};
|
|
@@ -10268,30 +10452,6 @@ var ServiceAppManager = class {
|
|
|
10268
10452
|
getServiceAppsPath = (workspacePath) => join(workspacePath, DEFAULT_SERVICE_APPS_DIR);
|
|
10269
10453
|
createGrantStore = () => new ServiceActionGrantStore(join(this.getServiceAppsPath(this.getWorkspacePath()), SERVICE_ACTION_GRANTS_FILE_NAME));
|
|
10270
10454
|
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
10455
|
listServiceAppDirNames = async (serviceAppsPath) => {
|
|
10296
10456
|
try {
|
|
10297
10457
|
return (await readdir(serviceAppsPath, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
@@ -10302,9 +10462,6 @@ var ServiceAppManager = class {
|
|
|
10302
10462
|
};
|
|
10303
10463
|
isMissingFileError = (error) => typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
10304
10464
|
};
|
|
10305
|
-
function toTitle(value) {
|
|
10306
|
-
return basename(value).replace(/[-_]+/g, " ").trim() || value;
|
|
10307
|
-
}
|
|
10308
10465
|
//#endregion
|
|
10309
10466
|
//#region src/managers/session-run.manager.ts
|
|
10310
10467
|
var MessageInbox = class {
|
|
@@ -11107,9 +11264,9 @@ var NcpAgentSessionMessageProjectionStore = class {
|
|
|
11107
11264
|
if (!meta) return null;
|
|
11108
11265
|
const uniqueTailMessages = deduplicateNcpAgentSessionTailMessages(tailMessages ?? []);
|
|
11109
11266
|
const tailById = new Map(uniqueTailMessages.map((message) => [message.id, message]));
|
|
11110
|
-
const messageOrdinals = await this.readMessageOrdinals(sessionId, meta);
|
|
11111
|
-
const additionalTailMessages = uniqueTailMessages.filter((message) => !messageOrdinals.has(message.id));
|
|
11112
|
-
const limit = Number.isFinite(requestedLimit) ? Math.min(200, Math.max(1, Math.trunc(requestedLimit))) :
|
|
11267
|
+
const messageOrdinals = uniqueTailMessages.length > 0 ? await this.readMessageOrdinals(sessionId, meta) : null;
|
|
11268
|
+
const additionalTailMessages = messageOrdinals ? uniqueTailMessages.filter((message) => !messageOrdinals.has(message.id)) : [];
|
|
11269
|
+
const limit = Number.isFinite(requestedLimit) ? Math.min(200, Math.max(1, Math.trunc(requestedLimit))) : 40;
|
|
11113
11270
|
const boundary = cursor ? decodeNcpAgentSessionMessageCursor(cursor, meta.total + 1) : meta.total + 1;
|
|
11114
11271
|
const includeTail = !cursor;
|
|
11115
11272
|
const stableLimit = includeTail ? Math.max(0, limit - additionalTailMessages.length) : limit;
|