@zhin.js/scaffold-wizard 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/adapter.d.ts +2 -2
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +1 -1
- package/lib/apply.d.ts +6 -6
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +29 -41
- package/lib/apply.js.map +1 -1
- package/lib/database-definitions.js +1 -1
- package/lib/decisions.d.ts.map +1 -1
- package/lib/decisions.js +10 -17
- package/lib/decisions.js.map +1 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/project-config-plan.d.ts +3 -0
- package/lib/project-config-plan.d.ts.map +1 -1
- package/lib/project-config-plan.js +38 -12
- package/lib/project-config-plan.js.map +1 -1
- package/lib/project-deps.d.ts +3 -1
- package/lib/project-deps.d.ts.map +1 -1
- package/lib/project-deps.js +3 -1
- package/lib/project-deps.js.map +1 -1
- package/lib/stack-version-ranges.d.ts +35 -0
- package/lib/stack-version-ranges.d.ts.map +1 -0
- package/lib/stack-version-ranges.js +144 -0
- package/lib/stack-version-ranges.js.map +1 -0
- package/lib/stack-version-ranges.json +112 -0
- package/lib/zhin-stack-deps.d.ts +4 -2
- package/lib/zhin-stack-deps.d.ts.map +1 -1
- package/lib/zhin-stack-deps.js +4 -2
- package/lib/zhin-stack-deps.js.map +1 -1
- package/package.json +4 -2
- package/src/adapter.ts +2 -2
- package/src/apply.ts +34 -46
- package/src/database-definitions.ts +1 -1
- package/src/decisions.ts +9 -17
- package/src/index.ts +7 -1
- package/src/project-config-plan.ts +39 -11
- package/src/project-deps.ts +3 -1
- package/src/stack-version-ranges.ts +160 -0
- package/src/zhin-stack-deps.ts +4 -2
- package/src/stack-versions.generated.json +0 -45
package/src/apply.ts
CHANGED
|
@@ -2,9 +2,9 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'yaml';
|
|
4
4
|
import { readPluginConfigurationMap } from '@zhin.js/plugin-runtime';
|
|
5
|
+
import { readApplicationPluginDeclaration } from '@zhin.js/runtime';
|
|
5
6
|
import type { AdapterSetupResult, AISetupConfig, DatabaseConfig, InitOptions } from './types.js';
|
|
6
7
|
import {
|
|
7
|
-
collectAdapterPluginConfigs,
|
|
8
8
|
collectAdapterPluginManifest,
|
|
9
9
|
generateAdapterEnvVars,
|
|
10
10
|
getAdapterDependencies,
|
|
@@ -14,6 +14,7 @@ import { generateDatabaseEnvVars, mergeEnvText } from './env.js';
|
|
|
14
14
|
import { getDatabaseDialectDefinition } from './database-definitions.js';
|
|
15
15
|
import { ensureDatabaseForAdapters, ensureDatabaseForAI, getAIDependencies } from './project-deps.js';
|
|
16
16
|
import { DEFAULT_CREATE_BOT_HTTP_PORT, ZHIN_STACK_VERSIONS } from './zhin-stack-deps.js';
|
|
17
|
+
import { withScaffoldVersionRanges } from './stack-version-ranges.js';
|
|
17
18
|
|
|
18
19
|
export function applyDatabaseToConfig(config: Record<string, unknown>, database: DatabaseConfig): void {
|
|
19
20
|
// 物化为可落盘对象:非 SQLite 连接参数引用 .env(${VAR}),明文密码不进配置文件
|
|
@@ -28,15 +29,34 @@ export function clonePluginConfigurationMap(
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function applyAdaptersToConfig(config: Record<string, unknown>, result: AdapterSetupResult): void {
|
|
31
|
-
const plugins = clonePluginConfigurationMap(config);
|
|
32
32
|
for (const instance of result.instances) {
|
|
33
|
-
plugins
|
|
34
|
-
|
|
35
|
-
plugins[instance.instanceKey],
|
|
36
|
-
|
|
37
|
-
);
|
|
33
|
+
const plugins = clonePluginConfigurationMap(config);
|
|
34
|
+
setPluginInstanceConfig(config, instance, mergeAdapterInstanceConfig(
|
|
35
|
+
instance.instanceKey, plugins[instance.instanceKey], instance.config,
|
|
36
|
+
));
|
|
38
37
|
}
|
|
39
|
-
|
|
38
|
+
if (result.instances.length === 0) clonePluginConfigurationMap(config);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Writes one instance using the document's explicitly selected protocol. */
|
|
42
|
+
export function setPluginInstanceConfig(
|
|
43
|
+
config: Record<string, unknown>,
|
|
44
|
+
instance: { instanceKey: string; package: string },
|
|
45
|
+
value: Record<string, unknown>,
|
|
46
|
+
): void {
|
|
47
|
+
const declaration = readApplicationPluginDeclaration(config);
|
|
48
|
+
const existing = declaration.instances.find(item => item.instanceKey === instance.instanceKey);
|
|
49
|
+
if (existing && existing.package !== instance.package) {
|
|
50
|
+
throw new Error(`Plugin instance ${instance.instanceKey} already belongs to ${existing.package}`);
|
|
51
|
+
}
|
|
52
|
+
const next = {
|
|
53
|
+
...config,
|
|
54
|
+
plugins: { ...(config.plugins as Record<string, string> | undefined), [instance.instanceKey]: instance.package },
|
|
55
|
+
[instance.instanceKey]: value,
|
|
56
|
+
};
|
|
57
|
+
readApplicationPluginDeclaration(next);
|
|
58
|
+
config.plugins = next.plugins;
|
|
59
|
+
config[instance.instanceKey] = value;
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
function endpointId(endpoint: unknown): string | undefined {
|
|
@@ -89,35 +109,6 @@ export function applyAIToConfig(config: Record<string, unknown>, ai: AISetupConf
|
|
|
89
109
|
config.ai = materializeAIConfig(ai);
|
|
90
110
|
}
|
|
91
111
|
|
|
92
|
-
/** 把 zhin.plugins 清单条目合并进项目 package.json(zhin setup 路径) */
|
|
93
|
-
export async function mergePluginManifestIntoPackageJson(
|
|
94
|
-
projectDir: string,
|
|
95
|
-
entries: Array<{ package: string; instanceKey: string }>,
|
|
96
|
-
): Promise<boolean> {
|
|
97
|
-
if (entries.length === 0) return false;
|
|
98
|
-
const pkgPath = path.join(projectDir, 'package.json');
|
|
99
|
-
if (!(await fs.pathExists(pkgPath))) return false;
|
|
100
|
-
|
|
101
|
-
const pkg = await fs.readJson(pkgPath);
|
|
102
|
-
const zhin = pkg.zhin && typeof pkg.zhin === 'object' && !Array.isArray(pkg.zhin)
|
|
103
|
-
? pkg.zhin
|
|
104
|
-
: { protocol: 1, type: 'plugin', entry: './plugin.ts' };
|
|
105
|
-
const manifest: Array<{ package: string; instanceKey: string }> = Array.isArray(zhin.plugins)
|
|
106
|
-
? [...zhin.plugins]
|
|
107
|
-
: [];
|
|
108
|
-
let changed = false;
|
|
109
|
-
for (const entry of entries) {
|
|
110
|
-
if (manifest.some((item) => item?.instanceKey === entry.instanceKey)) continue;
|
|
111
|
-
manifest.push({ package: entry.package, instanceKey: entry.instanceKey });
|
|
112
|
-
changed = true;
|
|
113
|
-
}
|
|
114
|
-
if (!changed && pkg.zhin) return false;
|
|
115
|
-
zhin.plugins = manifest;
|
|
116
|
-
pkg.zhin = zhin;
|
|
117
|
-
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
112
|
export async function appendWizardEnvVars(
|
|
122
113
|
projectDir: string,
|
|
123
114
|
adapters?: AdapterSetupResult,
|
|
@@ -135,10 +126,7 @@ export async function appendWizardEnvVars(
|
|
|
135
126
|
await fs.writeFile(envPath, mergeEnvText(existing, extra));
|
|
136
127
|
}
|
|
137
128
|
|
|
138
|
-
|
|
139
|
-
if (adapters) {
|
|
140
|
-
await mergePluginManifestIntoPackageJson(projectDir, collectAdapterPluginManifest(adapters));
|
|
141
|
-
}
|
|
129
|
+
|
|
142
130
|
}
|
|
143
131
|
|
|
144
132
|
export function collectWizardDependencies(
|
|
@@ -147,7 +135,7 @@ export function collectWizardDependencies(
|
|
|
147
135
|
const databaseDriver = options.database
|
|
148
136
|
? getDatabaseDialectDefinition(options.database.dialect)?.driver
|
|
149
137
|
: undefined;
|
|
150
|
-
return {
|
|
138
|
+
return withScaffoldVersionRanges({
|
|
151
139
|
...(options.adapters ? getAdapterDependencies(options.adapters) : {}),
|
|
152
140
|
...(databaseDriver ? { [databaseDriver.package]: databaseDriver.version } : {}),
|
|
153
141
|
...getAIDependencies(options.ai),
|
|
@@ -156,7 +144,7 @@ export function collectWizardDependencies(
|
|
|
156
144
|
'@zhin.js/tool': ZHIN_STACK_VERSIONS['@zhin.js/tool'],
|
|
157
145
|
'@zhin.js/prompt-section': ZHIN_STACK_VERSIONS['@zhin.js/prompt-section'],
|
|
158
146
|
} : {}),
|
|
159
|
-
};
|
|
147
|
+
});
|
|
160
148
|
}
|
|
161
149
|
|
|
162
150
|
/** 向导结果需要合并进 package.json zhin.features 的条目(与 create-zhin 路径对齐) */
|
|
@@ -295,11 +283,11 @@ function applyLifeAssistantAiExtras(ai: Record<string, unknown>): Record<string,
|
|
|
295
283
|
}
|
|
296
284
|
|
|
297
285
|
/**
|
|
298
|
-
*
|
|
286
|
+
* 构建 v2 扁平主配置(plugins 包映射 + 顶层实例业务配置)。
|
|
299
287
|
* 顶层键与 packages/im/runtime/src/config-composer.ts 的 effectiveSchema 对齐。
|
|
300
288
|
*/
|
|
301
289
|
export function buildRuntimeConfigDocument(options: InitOptions): Record<string, unknown> {
|
|
302
|
-
const doc: Record<string, unknown> = {};
|
|
290
|
+
const doc: Record<string, unknown> = { configVersion: 2, plugins: {} };
|
|
303
291
|
|
|
304
292
|
if (options.database) {
|
|
305
293
|
doc.database = materializeDatabaseConfig(options.database);
|
|
@@ -317,7 +305,7 @@ export function buildRuntimeConfigDocument(options: InitOptions): Record<string,
|
|
|
317
305
|
doc.ai = options.template === 'life-assistant' ? applyLifeAssistantAiExtras(ai) : ai;
|
|
318
306
|
}
|
|
319
307
|
|
|
320
|
-
|
|
308
|
+
if (options.adapters) applyAdaptersToConfig(doc, options.adapters);
|
|
321
309
|
|
|
322
310
|
return doc;
|
|
323
311
|
}
|
|
@@ -101,7 +101,7 @@ export const DATABASE_DIALECT_DEFINITIONS: readonly DatabaseDialectDefinition[]
|
|
|
101
101
|
dialect: 'mongodb',
|
|
102
102
|
name: 'MongoDB',
|
|
103
103
|
description: '文档数据库',
|
|
104
|
-
verifiedServer: 'MongoDB
|
|
104
|
+
verifiedServer: 'MongoDB 7',
|
|
105
105
|
driver: { package: 'mongodb', version: '^7.5.0' },
|
|
106
106
|
schema: {
|
|
107
107
|
type: 'object',
|
package/src/decisions.ts
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { formatEnvValue, mergeEnvText } from './env.js';
|
|
6
|
-
import { clonePluginConfigurationMap, mergeDependenciesIntoPackageJson,
|
|
6
|
+
import { clonePluginConfigurationMap, mergeDependenciesIntoPackageJson, setPluginInstanceConfig } from './apply.js';
|
|
7
7
|
import { isAiEnabledInConfig } from './project-deps.js';
|
|
8
8
|
|
|
9
9
|
export const TYPESAFE_DECISION_PACKAGE = '@zhin.js/service-typesafe';
|
|
@@ -35,7 +35,7 @@ export async function configureTypeSafeDecisions(config: Readonly<Record<string,
|
|
|
35
35
|
{ type: 'input', name: 'model', message: 'Jev 模型(生产校准后建议固定版本)', default: 'jev-latest', validate: (value: string) => value.trim().length > 0 || '请输入模型名称' },
|
|
36
36
|
{ type: 'select', name: 'mode', message: 'Skill / Tool 推荐运行方式', default: 'shadow', choices: [ { name: 'shadow:观察建议,评估后再启用', value: 'shadow' }, { name: 'active:启用推荐,权限仍由 Zhin 校验', value: 'active' } ] },
|
|
37
37
|
]);
|
|
38
|
-
const reference = record(
|
|
38
|
+
const reference = record(clonePluginConfigurationMap(config)[answers.instanceKey]).apiKey;
|
|
39
39
|
const existingVariable = typeof reference === 'string' ? /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(reference)?.[1] : undefined;
|
|
40
40
|
return { ...answers, envVar: existingVariable ?? decisionKeyVariable(answers.instanceKey) };
|
|
41
41
|
}
|
|
@@ -55,8 +55,7 @@ export function applyTypeSafeDecisionsToConfig(config: Record<string, unknown>,
|
|
|
55
55
|
if (!/^[a-z][a-z0-9-]*$/.test(setup.instanceKey) || !setup.model.trim() || !setup.apiKey.trim()) throw new Error('Invalid TypeSafe setup configuration.');
|
|
56
56
|
const plugins = clonePluginConfigurationMap(config);
|
|
57
57
|
const existing = record(plugins[setup.instanceKey]);
|
|
58
|
-
|
|
59
|
-
config.plugins = plugins;
|
|
58
|
+
setPluginInstanceConfig(config, { instanceKey: setup.instanceKey, package: TYPESAFE_DECISION_PACKAGE }, { ...existing, apiKey: '${' + envVariable(setup) + '}', model: setup.model.trim(), timeoutMs: 10000, maxRetries: 2, enabled: true });
|
|
60
59
|
const ai = record(config.ai);
|
|
61
60
|
const previousDecisions = record(ai.decisions);
|
|
62
61
|
config.ai = { ...ai, decisions: {
|
|
@@ -72,13 +71,6 @@ export function applyTypeSafeDecisionsToConfig(config: Record<string, unknown>,
|
|
|
72
71
|
|
|
73
72
|
export async function saveTypeSafeSetupDependencies(projectDir: string, setup: TypeSafeSetupConfig): Promise<void> {
|
|
74
73
|
const variable = envVariable(setup);
|
|
75
|
-
const pkgPath = path.join(projectDir, 'package.json');
|
|
76
|
-
const pkg = await fs.readJson(pkgPath);
|
|
77
|
-
const existingManifest = record(pkg.zhin).plugins;
|
|
78
|
-
if (Array.isArray(existingManifest) && existingManifest.some(entry => record(entry).instanceKey === setup.instanceKey && record(entry).package !== TYPESAFE_DECISION_PACKAGE)) {
|
|
79
|
-
throw new Error('Decision plugin instance already belongs to another package; choose a different instanceKey.');
|
|
80
|
-
}
|
|
81
|
-
await mergePluginManifestIntoPackageJson(projectDir, [{ package: TYPESAFE_DECISION_PACKAGE, instanceKey: setup.instanceKey }]);
|
|
82
74
|
await mergeDependenciesIntoPackageJson(projectDir, { [TYPESAFE_DECISION_PACKAGE]: '^1.0.0' });
|
|
83
75
|
const envPath = path.join(projectDir, '.env');
|
|
84
76
|
const envExists = await fs.pathExists(envPath);
|
|
@@ -116,14 +108,14 @@ export function diagnoseDecisionConfig(config: Readonly<Record<string, unknown>>
|
|
|
116
108
|
// Nested Plugin owners are resolved from the actual runtime graph during activation.
|
|
117
109
|
if (key.includes('/')) return issues;
|
|
118
110
|
const pkg = record(packageJson);
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
issues.push('决策插件实例未在 package.json#zhin.plugins 唯一声明。');
|
|
111
|
+
const declared = record(config.plugins);
|
|
112
|
+
if (typeof declared[key] !== 'string') {
|
|
113
|
+
issues.push('决策插件实例未在主配置 plugins 中声明。');
|
|
123
114
|
return issues;
|
|
124
115
|
}
|
|
125
|
-
if (
|
|
126
|
-
|
|
116
|
+
if (Array.isArray(config.disable_plugins) && config.disable_plugins.includes(key)) issues.push('绑定的决策插件已停用。');
|
|
117
|
+
if (declared[key] === TYPESAFE_DECISION_PACKAGE) {
|
|
118
|
+
const configForProvider = record(config[key]);
|
|
127
119
|
if (configForProvider.enabled === false) issues.push('绑定的 TypeSafe 插件已禁用。');
|
|
128
120
|
if (typeof configForProvider.apiKey !== 'string' || !configForProvider.apiKey.trim()) issues.push('TypeSafe 插件缺少 apiKey 环境变量引用。');
|
|
129
121
|
if (!Object.hasOwn({ ...record(pkg.dependencies), ...record(pkg.devDependencies) }, TYPESAFE_DECISION_PACKAGE)) issues.push('package.json 缺少 @zhin.js/service-typesafe 依赖。');
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,13 @@ export {
|
|
|
70
70
|
} from './project-deps.js';
|
|
71
71
|
export type { AiStackIncompatibility } from './project-deps.js';
|
|
72
72
|
|
|
73
|
+
export {
|
|
74
|
+
FALLBACK_DEPENDENCY_RANGE,
|
|
75
|
+
scaffoldDependencyRange,
|
|
76
|
+
withScaffoldVersionRanges,
|
|
77
|
+
} from './stack-version-ranges.js';
|
|
78
|
+
export type { StackVersionRanges } from './stack-version-ranges.js';
|
|
79
|
+
|
|
73
80
|
export {
|
|
74
81
|
ZHIN_STACK_VERSIONS,
|
|
75
82
|
DEFAULT_CREATE_BOT_HTTP_PORT,
|
|
@@ -128,7 +135,6 @@ export {
|
|
|
128
135
|
collectWizardFeatures,
|
|
129
136
|
mergeDependenciesIntoPackageJson,
|
|
130
137
|
mergeFeaturesIntoPackageJson,
|
|
131
|
-
mergePluginManifestIntoPackageJson,
|
|
132
138
|
finalizeWizardOptions,
|
|
133
139
|
applyWizardOptionsToConfig,
|
|
134
140
|
clonePluginConfigurationMap,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import yaml from 'yaml';
|
|
4
|
+
import { createConfigDocument, readSourcePluginOrder } from '@zhin.js/config-file';
|
|
5
|
+
import { readApplicationPluginDeclaration } from '@zhin.js/runtime';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
7
|
import {
|
|
5
8
|
readPluginConfigurationMap,
|
|
6
9
|
ROOT_CONFIG_FILE_NAMES,
|
|
@@ -32,6 +35,8 @@ export interface LoadedProjectConfig {
|
|
|
32
35
|
format?: ProjectConfigFormat;
|
|
33
36
|
config: Record<string, unknown>;
|
|
34
37
|
writable: boolean;
|
|
38
|
+
/** Exact preview source: comments and declaration order are concurrency inputs. */
|
|
39
|
+
source?: string;
|
|
35
40
|
message?: string;
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -55,6 +60,7 @@ export interface ProjectConfigPlan {
|
|
|
55
60
|
relativePath?: string;
|
|
56
61
|
format?: ProjectConfigFormat;
|
|
57
62
|
writable: boolean;
|
|
63
|
+
expectedSource?: string;
|
|
58
64
|
before: Record<string, unknown>;
|
|
59
65
|
after: Record<string, unknown>;
|
|
60
66
|
mutations: string[];
|
|
@@ -128,9 +134,10 @@ export function loadProjectConfig(cwd = process.cwd(), configPath?: string): Loa
|
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
137
|
+
readSourcePluginOrder(content, format === 'json' ? 'JSON' : 'YAML');
|
|
131
138
|
const config = parseConfig(content, format);
|
|
132
139
|
try {
|
|
133
|
-
|
|
140
|
+
readApplicationPluginDeclaration(config, relativePath);
|
|
134
141
|
} catch (error) {
|
|
135
142
|
return {
|
|
136
143
|
status: 'unsupported',
|
|
@@ -150,23 +157,25 @@ export function loadProjectConfig(cwd = process.cwd(), configPath?: string): Loa
|
|
|
150
157
|
relativePath,
|
|
151
158
|
format,
|
|
152
159
|
config,
|
|
160
|
+
source: content,
|
|
153
161
|
writable: true,
|
|
154
162
|
};
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
function ensurePlugins(config: Record<string, unknown>, pluginsToAdd: readonly string[], mutations: string[]): void {
|
|
158
|
-
const
|
|
159
|
-
|
|
166
|
+
const declaration = readApplicationPluginDeclaration(config);
|
|
167
|
+
const plugins = { ...(config.plugins as Record<string, string> | undefined) };
|
|
160
168
|
for (const pkg of pluginsToAdd) {
|
|
161
169
|
const instanceKey = packageToInstanceKey(pkg);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
config.plugins = plugins;
|
|
170
|
+
const existing = declaration.instances.find(item => item.instanceKey === instanceKey);
|
|
171
|
+
if (existing && existing.package !== pkg) throw new Error(`Plugin instance ${instanceKey} already belongs to ${existing.package}`);
|
|
172
|
+
if (!existing) {
|
|
173
|
+
plugins[instanceKey] = pkg;
|
|
174
|
+
mutations.push(`added ${instanceKey} (${pkg}) to plugins`);
|
|
175
|
+
}
|
|
169
176
|
}
|
|
177
|
+
config.plugins = plugins;
|
|
178
|
+
readApplicationPluginDeclaration(config);
|
|
170
179
|
}
|
|
171
180
|
|
|
172
181
|
function ensureHttp(config: Record<string, unknown>, mutations: string[]): void {
|
|
@@ -332,6 +341,7 @@ export function createProjectConfigPlan(options: ProjectConfigPlanOptions): Proj
|
|
|
332
341
|
relativePath: loaded.relativePath,
|
|
333
342
|
format: loaded.format,
|
|
334
343
|
writable: loaded.writable,
|
|
344
|
+
expectedSource: 'source' in loaded ? loaded.source : (loaded.configPath && fs.existsSync(loaded.configPath) ? fs.readFileSync(loaded.configPath, 'utf8') : undefined),
|
|
335
345
|
before,
|
|
336
346
|
after,
|
|
337
347
|
mutations,
|
|
@@ -354,7 +364,25 @@ export function renderProjectConfigPatch(plan: ProjectConfigPlan): string {
|
|
|
354
364
|
export async function applyProjectConfigPlan(plan: ProjectConfigPlan): Promise<boolean> {
|
|
355
365
|
if (!plan.changed) return false;
|
|
356
366
|
if (!plan.writable || !plan.configPath || !plan.format) return false;
|
|
357
|
-
|
|
367
|
+
const document = createConfigDocument(plan.configPath);
|
|
368
|
+
const snapshot = await document.readSource();
|
|
369
|
+
if ((plan.expectedSource !== undefined && snapshot.source !== plan.expectedSource) || !isDeepStrictEqual(snapshot.document, plan.before)) throw new Error('配置已在预览后发生变更,请重新生成配置计划。');
|
|
370
|
+
const keys = new Set([...Object.keys(plan.before), ...Object.keys(plan.after)]);
|
|
371
|
+
const patches = [...keys].filter(key => !isDeepStrictEqual(plan.before[key], plan.after[key])).flatMap(key => {
|
|
372
|
+
if (key === 'plugins') {
|
|
373
|
+
const before = plan.before.plugins as Record<string, string> | undefined;
|
|
374
|
+
const after = plan.after.plugins as Record<string, string> | undefined;
|
|
375
|
+
return [...new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})])]
|
|
376
|
+
.filter(alias => before?.[alias] !== after?.[alias])
|
|
377
|
+
.map(alias => Object.hasOwn(after ?? {}, alias)
|
|
378
|
+
? { op: 'set' as const, path: ['plugins', alias], value: after![alias] }
|
|
379
|
+
: { op: 'remove' as const, path: ['plugins', alias] });
|
|
380
|
+
}
|
|
381
|
+
return [Object.hasOwn(plan.after, key)
|
|
382
|
+
? { op: 'set' as const, path: [key], value: plan.after[key] }
|
|
383
|
+
: { op: 'remove' as const, path: [key] }];
|
|
384
|
+
});
|
|
385
|
+
await (await document.prepare(snapshot, patches)).commit();
|
|
358
386
|
return true;
|
|
359
387
|
}
|
|
360
388
|
|
package/src/project-deps.ts
CHANGED
|
@@ -5,7 +5,9 @@ import type { AISetupConfig } from './ai.js';
|
|
|
5
5
|
import type { InitOptions } from './types.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Diagnostic baseline for the AI stack (any declared version is accepted).
|
|
9
|
+
* Scaffolds write derived ranges instead of `latest`: `withScaffoldVersionRanges()` uses the
|
|
10
|
+
* `@zhin.js/agent` release version and the third-party ranges Zhin declares as peers.
|
|
9
11
|
*/
|
|
10
12
|
export const AI_STACK_VERSIONS = {
|
|
11
13
|
'@zhin.js/agent': 'latest',
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Version ranges that scaffolds write into new bot projects instead of `latest`.
|
|
8
|
+
*
|
|
9
|
+
* They are derived from the real package versions of the release that ships this package:
|
|
10
|
+
* - published package: `prepack` writes `lib/stack-version-ranges.json` from the monorepo,
|
|
11
|
+
* so the ranges match the versions released together with it;
|
|
12
|
+
* - inside the zhin monorepo (source / tests / local builds): read live from the workspace.
|
|
13
|
+
* Anything unknown keeps the `latest` fallback, so a scaffold never writes an invalid range.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** File written next to the compiled module at pack time. */
|
|
17
|
+
export const STACK_VERSION_RANGES_FILE = 'stack-version-ranges.json';
|
|
18
|
+
|
|
19
|
+
/** Written when no derived range is known for a package. */
|
|
20
|
+
export const FALLBACK_DEPENDENCY_RANGE = 'latest';
|
|
21
|
+
|
|
22
|
+
/** Zhin packages whose peerDependencies define the supported third-party AI stack. */
|
|
23
|
+
const THIRD_PARTY_RANGE_SOURCES = ['@zhin.js/ai', '@zhin.js/agent'] as const;
|
|
24
|
+
|
|
25
|
+
export interface StackVersionRanges {
|
|
26
|
+
readonly [packageName: string]: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface WorkspaceManifest {
|
|
30
|
+
readonly name?: string;
|
|
31
|
+
readonly version?: string;
|
|
32
|
+
readonly private?: boolean;
|
|
33
|
+
readonly peerDependencies?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isZhinPackageName(name: string): boolean {
|
|
37
|
+
return name === 'zhin.js' || name.startsWith('@zhin.js/');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `1.2.3-beta.1` → `^1.2.3-beta.1`; anything that is not a version yields `undefined`. */
|
|
41
|
+
export function caretRangeFromVersion(version: string | undefined): string | undefined {
|
|
42
|
+
if (typeof version !== 'string') return undefined;
|
|
43
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/u.exec(version.trim());
|
|
44
|
+
if (!match) return undefined;
|
|
45
|
+
return `^${match[0]}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readManifest(directory: string): WorkspaceManifest | undefined {
|
|
49
|
+
const manifestPath = path.join(directory, 'package.json');
|
|
50
|
+
if (!fs.existsSync(manifestPath)) return undefined;
|
|
51
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as WorkspaceManifest;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function workspacePackageDirectories(workspaceRoot: string): string[] {
|
|
55
|
+
const workspaceFile = path.join(workspaceRoot, 'pnpm-workspace.yaml');
|
|
56
|
+
const workspace = parseYaml(fs.readFileSync(workspaceFile, 'utf8')) as { packages?: unknown };
|
|
57
|
+
const patterns = Array.isArray(workspace.packages)
|
|
58
|
+
? workspace.packages.filter((pattern): pattern is string => typeof pattern === 'string')
|
|
59
|
+
: [];
|
|
60
|
+
const directories: string[] = [];
|
|
61
|
+
for (const pattern of patterns) {
|
|
62
|
+
if (!pattern.endsWith('/*')) {
|
|
63
|
+
directories.push(path.join(workspaceRoot, pattern));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const parent = path.join(workspaceRoot, pattern.slice(0, -2));
|
|
67
|
+
if (!fs.existsSync(parent)) continue;
|
|
68
|
+
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
|
|
69
|
+
if (entry.isDirectory()) directories.push(path.join(parent, entry.name));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return directories;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Collect ranges from a zhin monorepo checkout: `^<version>` for every published Zhin package,
|
|
77
|
+
* plus the third-party AI ranges Zhin itself declares as peers.
|
|
78
|
+
*/
|
|
79
|
+
export function collectStackVersionRanges(workspaceRoot: string): StackVersionRanges {
|
|
80
|
+
const ranges: Record<string, string> = {};
|
|
81
|
+
const manifestsByName = new Map<string, WorkspaceManifest>();
|
|
82
|
+
for (const directory of workspacePackageDirectories(workspaceRoot)) {
|
|
83
|
+
const manifest = readManifest(directory);
|
|
84
|
+
if (!manifest?.name) continue;
|
|
85
|
+
manifestsByName.set(manifest.name, manifest);
|
|
86
|
+
if (manifest.private === true) continue;
|
|
87
|
+
if (!isZhinPackageName(manifest.name)) continue;
|
|
88
|
+
const range = caretRangeFromVersion(manifest.version);
|
|
89
|
+
if (range) ranges[manifest.name] = range;
|
|
90
|
+
}
|
|
91
|
+
for (const sourceName of THIRD_PARTY_RANGE_SOURCES) {
|
|
92
|
+
const peers = manifestsByName.get(sourceName)?.peerDependencies ?? {};
|
|
93
|
+
for (const [peerName, peerRange] of Object.entries(peers)) {
|
|
94
|
+
if (isZhinPackageName(peerName)) continue;
|
|
95
|
+
if (peerRange.startsWith('workspace:')) continue;
|
|
96
|
+
ranges[peerName] ??= peerRange;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const sortedEntries = Object.entries(ranges).sort(([left], [right]) => left.localeCompare(right));
|
|
100
|
+
return Object.freeze(Object.fromEntries(sortedEntries));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Nearest ancestor that is the zhin monorepo root, if this module runs from a checkout. */
|
|
104
|
+
export function findZhinWorkspaceRoot(startDirectory: string): string | undefined {
|
|
105
|
+
let directory = startDirectory;
|
|
106
|
+
for (;;) {
|
|
107
|
+
const isWorkspaceRoot = fs.existsSync(path.join(directory, 'pnpm-workspace.yaml'))
|
|
108
|
+
&& fs.existsSync(path.join(directory, 'packages/toolkit/scaffold-wizard/package.json'));
|
|
109
|
+
if (isWorkspaceRoot) return directory;
|
|
110
|
+
const parent = path.dirname(directory);
|
|
111
|
+
if (parent === directory) return undefined;
|
|
112
|
+
directory = parent;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readPackedRanges(moduleDirectory: string): StackVersionRanges | undefined {
|
|
117
|
+
const rangesFile = path.join(moduleDirectory, STACK_VERSION_RANGES_FILE);
|
|
118
|
+
if (!fs.existsSync(rangesFile)) return undefined;
|
|
119
|
+
return Object.freeze(JSON.parse(fs.readFileSync(rangesFile, 'utf8')) as StackVersionRanges);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let cachedRanges: StackVersionRanges | undefined;
|
|
123
|
+
|
|
124
|
+
/** Ranges for this installation: packed file, then a live monorepo checkout, else none. */
|
|
125
|
+
export function loadStackVersionRanges(): StackVersionRanges {
|
|
126
|
+
if (cachedRanges) return cachedRanges;
|
|
127
|
+
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
128
|
+
const packedRanges = readPackedRanges(moduleDirectory);
|
|
129
|
+
if (packedRanges) {
|
|
130
|
+
cachedRanges = packedRanges;
|
|
131
|
+
return cachedRanges;
|
|
132
|
+
}
|
|
133
|
+
const workspaceRoot = findZhinWorkspaceRoot(moduleDirectory);
|
|
134
|
+
cachedRanges = workspaceRoot ? collectStackVersionRanges(workspaceRoot) : Object.freeze({});
|
|
135
|
+
return cachedRanges;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Range to write for one package; `latest` only when no release version is known. */
|
|
139
|
+
export function scaffoldDependencyRange(
|
|
140
|
+
packageName: string,
|
|
141
|
+
ranges: StackVersionRanges = loadStackVersionRanges(),
|
|
142
|
+
): string {
|
|
143
|
+
return ranges[packageName] ?? FALLBACK_DEPENDENCY_RANGE;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Replace `latest` entries with derived ranges before a scaffold writes package.json.
|
|
148
|
+
* Explicit ranges (database drivers, tooling) and packages without a known version stay as they are.
|
|
149
|
+
*/
|
|
150
|
+
export function withScaffoldVersionRanges(
|
|
151
|
+
dependencies: Readonly<Record<string, string>>,
|
|
152
|
+
ranges: StackVersionRanges = loadStackVersionRanges(),
|
|
153
|
+
): Record<string, string> {
|
|
154
|
+
const resolved: Record<string, string> = {};
|
|
155
|
+
for (const [packageName, range] of Object.entries(dependencies)) {
|
|
156
|
+
const isFloating = range === FALLBACK_DEPENDENCY_RANGE;
|
|
157
|
+
resolved[packageName] = isFloating ? scaffoldDependencyRange(packageName, ranges) : range;
|
|
158
|
+
}
|
|
159
|
+
return resolved;
|
|
160
|
+
}
|
package/src/zhin-stack-deps.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { DATABASE_DRIVER_VERSIONS, DATABASE_PACKAGES } from './types.js';
|
|
|
5
5
|
import { findMissingPackageDependencies, findUnresolvedPackageInstalls, isAiEnabledInConfig, type PackageJsonLike } from './project-deps.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Diagnostic baseline for doctor / config check: any declared version of these packages is accepted
|
|
9
|
+
* (`latest`), so existing projects are never flagged because of a newer release.
|
|
10
|
+
* Scaffolds do not write these values as-is: `withScaffoldVersionRanges()` replaces `latest` with the
|
|
11
|
+
* caret range of the version released together with this package (see stack-version-ranges.ts).
|
|
9
12
|
* Database drivers use the live-tested ranges declared by the database definitions.
|
|
10
|
-
* WARNING: `latest` is not reproducible; pin Zhin package versions for production.
|
|
11
13
|
*/
|
|
12
14
|
export const ZHIN_STACK_VERSIONS = {
|
|
13
15
|
'zhin.js': 'latest',
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$comment": "Auto-generated by scripts/sync-scaffold-deps.mjs — do not edit; run pnpm sync:scaffold-deps",
|
|
3
|
-
"zhinStack": {
|
|
4
|
-
"zhin.js": "^5.0.0",
|
|
5
|
-
"@zhin.js/cli": "^2.0.0",
|
|
6
|
-
"@zhin.js/agent": "^1.0.5",
|
|
7
|
-
"@zhin.js/database": "^1.0.77",
|
|
8
|
-
"@zhin.js/client": "^2.1.0",
|
|
9
|
-
"@zhin.js/contract": "^1.0.5",
|
|
10
|
-
"@zhin.js/speech": "^2.0.0",
|
|
11
|
-
"@zhin.js/html-renderer": "^2.0.0",
|
|
12
|
-
"@zhin.js/mcp": "^5.0.0",
|
|
13
|
-
"@zhin.js/adapter-sandbox": "^6.0.0",
|
|
14
|
-
"@zhin.js/adapter-dingtalk": "^5.0.0",
|
|
15
|
-
"@zhin.js/adapter-discord": "^6.0.0",
|
|
16
|
-
"@zhin.js/adapter-email": "^4.0.0",
|
|
17
|
-
"@zhin.js/adapter-github": "^4.0.0",
|
|
18
|
-
"@zhin.js/adapter-icqq": "^7.0.0",
|
|
19
|
-
"@zhin.js/adapter-kook": "^6.0.0",
|
|
20
|
-
"@zhin.js/adapter-lark": "^5.0.0",
|
|
21
|
-
"@zhin.js/adapter-line": "^3.0.0",
|
|
22
|
-
"@zhin.js/adapter-milky": "^5.0.0",
|
|
23
|
-
"@zhin.js/adapter-napcat": "^5.0.0",
|
|
24
|
-
"@zhin.js/adapter-onebot11": "^5.0.0",
|
|
25
|
-
"@zhin.js/adapter-onebot12": "^4.0.0",
|
|
26
|
-
"@zhin.js/adapter-qq": "^6.0.0",
|
|
27
|
-
"@zhin.js/adapter-satori": "^4.0.0",
|
|
28
|
-
"@zhin.js/adapter-slack": "^5.0.0",
|
|
29
|
-
"@zhin.js/adapter-telegram": "^6.0.0",
|
|
30
|
-
"@zhin.js/adapter-wechat-mp": "^4.0.0",
|
|
31
|
-
"@zhin.js/adapter-wecom": "^3.0.0",
|
|
32
|
-
"@zhin.js/adapter-weixin-ilink": "^4.0.0"
|
|
33
|
-
},
|
|
34
|
-
"aiStack": {
|
|
35
|
-
"@zhin.js/agent": "^1.0.5",
|
|
36
|
-
"ai": "^7.0.0",
|
|
37
|
-
"zod": "^4.0.0",
|
|
38
|
-
"@ai-sdk/openai": "^4.0.8",
|
|
39
|
-
"@ai-sdk/anthropic": "^4.0.9",
|
|
40
|
-
"@ai-sdk/google": "^4.0.9",
|
|
41
|
-
"@ai-sdk/deepseek": "^3.0.5",
|
|
42
|
-
"@ai-sdk/openai-compatible": "^3.0.5",
|
|
43
|
-
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
44
|
-
}
|
|
45
|
-
}
|