@zhin.js/scaffold-wizard 0.1.3 → 0.1.5
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-configurers.d.ts +7 -7
- package/lib/adapter-configurers.d.ts.map +1 -1
- package/lib/adapter-configurers.js +55 -55
- package/lib/adapter-configurers.js.map +1 -1
- package/lib/adapter.d.ts +8 -8
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +42 -42
- package/lib/adapter.js.map +1 -1
- package/lib/ai.d.ts.map +1 -1
- package/lib/ai.js +13 -10
- package/lib/ai.js.map +1 -1
- package/lib/apply.js +3 -3
- package/lib/apply.js.map +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -2
- package/lib/index.js.map +1 -1
- package/lib/project-deps.d.ts +36 -1
- package/lib/project-deps.d.ts.map +1 -1
- package/lib/project-deps.js +158 -2
- package/lib/project-deps.js.map +1 -1
- package/lib/types.d.ts +1 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/package.json +7 -2
- package/src/adapter-configurers.ts +58 -58
- package/src/adapter.ts +47 -47
- package/src/ai.ts +13 -10
- package/src/apply.ts +3 -3
- package/src/index.ts +10 -3
- package/src/project-deps.ts +179 -2
- package/src/types.ts +1 -0
package/src/adapter.ts
CHANGED
|
@@ -3,18 +3,18 @@ import chalk from 'chalk';
|
|
|
3
3
|
import {
|
|
4
4
|
ADAPTERS_DOCS_URL,
|
|
5
5
|
adapterDocsUrl,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
configureDiscordEndpoint,
|
|
7
|
+
configureGitHubEndpoint,
|
|
8
8
|
configureOneBot11Bot,
|
|
9
9
|
configureQQBot,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
configureSlackEndpoint,
|
|
11
|
+
configureTelegramEndpoint,
|
|
12
12
|
} from './adapter-configurers.js';
|
|
13
13
|
|
|
14
14
|
export interface AdapterSetupResult {
|
|
15
15
|
packages: string[];
|
|
16
16
|
plugins: string[];
|
|
17
|
-
|
|
17
|
+
endpoints: Array<Record<string, any>>;
|
|
18
18
|
envVars: Record<string, string>;
|
|
19
19
|
/** 所选适配器需要 database 插件(如 GitHub 订阅/OAuth 表) */
|
|
20
20
|
requiresDatabase?: boolean;
|
|
@@ -33,7 +33,7 @@ interface AdapterDefinition {
|
|
|
33
33
|
/** 文档:zhin.js.org/adapters/<slug>;索引见 ADAPTERS_DOCS_URL */
|
|
34
34
|
docUrl?: string;
|
|
35
35
|
requiresDatabase?: boolean;
|
|
36
|
-
configure?: (ctx: import('./adapter-configurers.js').
|
|
36
|
+
configure?: (ctx: import('./adapter-configurers.js').EndpointConfigureContext) => Promise<Record<string, unknown>>;
|
|
37
37
|
fields: AdapterField[];
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -89,10 +89,10 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
89
89
|
package: '@zhin.js/adapter-kook',
|
|
90
90
|
plugin: '@zhin.js/adapter-kook',
|
|
91
91
|
needsHttp: true,
|
|
92
|
-
description: 'KOOK 语音平台
|
|
93
|
-
setupHint: '在 KOOK 开发者中心创建
|
|
92
|
+
description: 'KOOK 语音平台 Endpoint',
|
|
93
|
+
setupHint: '在 KOOK 开发者中心创建 Endpoint 并复制 Token;需配置 Webhook 回调地址(公网或内网穿透)。',
|
|
94
94
|
fields: [
|
|
95
|
-
{ key: 'token', message: 'KOOK
|
|
95
|
+
{ key: 'token', message: 'KOOK Endpoint Token:', required: true, type: 'password', envKey: 'KOOK_TOKEN' },
|
|
96
96
|
],
|
|
97
97
|
},
|
|
98
98
|
{
|
|
@@ -104,7 +104,7 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
104
104
|
description: 'Discord(Gateway 或 Interactions)',
|
|
105
105
|
setupHint: 'Developer Portal 创建 Bot;Gateway 用 Token,Interactions 需 Application ID + Public Key。',
|
|
106
106
|
docUrl: adapterDocsUrl('discord'),
|
|
107
|
-
configure:
|
|
107
|
+
configure: configureDiscordEndpoint,
|
|
108
108
|
fields: [],
|
|
109
109
|
},
|
|
110
110
|
{
|
|
@@ -116,7 +116,7 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
116
116
|
description: 'Telegram Bot(polling / webhook 可选)',
|
|
117
117
|
setupHint: '@BotFather 获取 Token;polling 适合本地,webhook 需 HTTPS 公网域名。',
|
|
118
118
|
docUrl: adapterDocsUrl('telegram'),
|
|
119
|
-
configure:
|
|
119
|
+
configure: configureTelegramEndpoint,
|
|
120
120
|
fields: [],
|
|
121
121
|
},
|
|
122
122
|
{
|
|
@@ -128,7 +128,7 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
128
128
|
description: 'Slack(Socket Mode 或 HTTP Events)',
|
|
129
129
|
setupHint: 'Socket Mode 无需公网;HTTP 模式需配置 Event Subscriptions URL。',
|
|
130
130
|
docUrl: adapterDocsUrl('slack'),
|
|
131
|
-
configure:
|
|
131
|
+
configure: configureSlackEndpoint,
|
|
132
132
|
fields: [],
|
|
133
133
|
},
|
|
134
134
|
{
|
|
@@ -213,19 +213,19 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
213
213
|
setupHint: 'GitHub App 或 gh CLI;Webhook 用 /pub/github/webhook;需 database 存订阅与 OAuth。',
|
|
214
214
|
docUrl: adapterDocsUrl('github'),
|
|
215
215
|
requiresDatabase: true,
|
|
216
|
-
configure:
|
|
216
|
+
configure: configureGitHubEndpoint,
|
|
217
217
|
fields: [],
|
|
218
218
|
},
|
|
219
219
|
];
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
* 适配器选择与
|
|
222
|
+
* 适配器选择与 Endpoint 配置引导向导
|
|
223
223
|
*/
|
|
224
224
|
export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
225
225
|
console.log('');
|
|
226
226
|
console.log(chalk.blue('🔌 配置聊天适配器'));
|
|
227
227
|
console.log(chalk.gray(' Sandbox 适合本地调试;生产环境请额外勾选目标 IM 平台。'));
|
|
228
|
-
console.log(chalk.gray(' 带 Webhook 的适配器需要
|
|
228
|
+
console.log(chalk.gray(' 带 Webhook 的适配器需要 Endpoint 可被公网访问(或使用内网穿透)。'));
|
|
229
229
|
|
|
230
230
|
const { selectedAdapters } = await inquirer.prompt([
|
|
231
231
|
{
|
|
@@ -266,7 +266,7 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
266
266
|
const result: AdapterSetupResult = {
|
|
267
267
|
packages: [],
|
|
268
268
|
plugins: [],
|
|
269
|
-
|
|
269
|
+
endpoints: [],
|
|
270
270
|
envVars: {},
|
|
271
271
|
requiresDatabase: false,
|
|
272
272
|
};
|
|
@@ -303,7 +303,7 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
303
303
|
|
|
304
304
|
if (adapterDef.configure) {
|
|
305
305
|
let markedDb = false;
|
|
306
|
-
const
|
|
306
|
+
const endpointConfig = await adapterDef.configure({
|
|
307
307
|
envVars: result.envVars,
|
|
308
308
|
markRequiresDatabase: () => {
|
|
309
309
|
markedDb = true;
|
|
@@ -311,11 +311,11 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
311
311
|
},
|
|
312
312
|
});
|
|
313
313
|
if (markedDb) result.requiresDatabase = true;
|
|
314
|
-
result.
|
|
314
|
+
result.endpoints.push(endpointConfig);
|
|
315
315
|
continue;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
const
|
|
318
|
+
const endpointConfig: Record<string, any> = {
|
|
319
319
|
context: adapterDef.value,
|
|
320
320
|
};
|
|
321
321
|
|
|
@@ -343,13 +343,13 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
343
343
|
if (field.envKey) {
|
|
344
344
|
// 敏感信息存 .env,配置引用环境变量
|
|
345
345
|
result.envVars[field.envKey] = value || '';
|
|
346
|
-
|
|
346
|
+
endpointConfig[field.key] = `\${${field.envKey}}`;
|
|
347
347
|
} else {
|
|
348
|
-
|
|
348
|
+
endpointConfig[field.key] = value;
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
result.
|
|
352
|
+
result.endpoints.push(endpointConfig);
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
return result;
|
|
@@ -370,7 +370,7 @@ export function generateAdapterEnvVars(result: AdapterSetupResult): string {
|
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
/**
|
|
373
|
-
* 将
|
|
373
|
+
* 将 Endpoint 配置项格式化为 YAML 行
|
|
374
374
|
*/
|
|
375
375
|
function appendYamlValue(lines: string[], key: string, value: unknown, indent: number): void {
|
|
376
376
|
const pad = ' '.repeat(indent);
|
|
@@ -405,20 +405,20 @@ function appendYamlValue(lines: string[], key: string, value: unknown, indent: n
|
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
/**
|
|
408
|
-
* 生成 YAML 格式的
|
|
408
|
+
* 生成 YAML 格式的 endpoints 配置段
|
|
409
409
|
*/
|
|
410
|
-
export function
|
|
411
|
-
if (result.
|
|
410
|
+
export function generateEndpointsConfigYaml(result: AdapterSetupResult): string {
|
|
411
|
+
if (result.endpoints.length === 0) {
|
|
412
412
|
return `
|
|
413
413
|
|
|
414
|
-
# Sandbox:Remote Console 打开「沙盒」页并通过 WebSocket 连接时自动创建
|
|
415
|
-
|
|
414
|
+
# Sandbox:Remote Console 打开「沙盒」页并通过 WebSocket 连接时自动创建 Endpoint
|
|
415
|
+
endpoints: []
|
|
416
416
|
`;
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
-
const lines: string[] = ['', '
|
|
420
|
-
for (const
|
|
421
|
-
const { context, ...config } =
|
|
419
|
+
const lines: string[] = ['', 'endpoints:'];
|
|
420
|
+
for (const entry of result.endpoints) {
|
|
421
|
+
const { context, ...config } = entry;
|
|
422
422
|
lines.push(` - context: ${context}`);
|
|
423
423
|
for (const [key, value] of Object.entries(config)) {
|
|
424
424
|
appendYamlValue(lines, key, value, 2);
|
|
@@ -429,15 +429,15 @@ bots: []
|
|
|
429
429
|
}
|
|
430
430
|
|
|
431
431
|
/**
|
|
432
|
-
* 生成 TOML 格式的
|
|
432
|
+
* 生成 TOML 格式的 endpoints 配置段
|
|
433
433
|
*/
|
|
434
|
-
export function
|
|
435
|
-
if (result.
|
|
434
|
+
export function generateEndpointsConfigToml(result: AdapterSetupResult): string {
|
|
435
|
+
if (result.endpoints.length === 0) return '';
|
|
436
436
|
|
|
437
437
|
const lines: string[] = [''];
|
|
438
|
-
for (const
|
|
439
|
-
lines.push('[[
|
|
440
|
-
for (const [key, value] of Object.entries(
|
|
438
|
+
for (const entry of result.endpoints) {
|
|
439
|
+
lines.push('[[endpoints]]');
|
|
440
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
441
441
|
if (typeof value === 'string') {
|
|
442
442
|
lines.push(`${key} = "${value.replace(/"/g, '\\"')}"`);
|
|
443
443
|
} else {
|
|
@@ -450,11 +450,11 @@ export function generateBotsConfigToml(result: AdapterSetupResult): string {
|
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
/**
|
|
453
|
-
* 生成 JSON 格式的
|
|
453
|
+
* 生成 JSON 格式的 endpoints 配置段
|
|
454
454
|
*/
|
|
455
|
-
export function
|
|
456
|
-
if (result.
|
|
457
|
-
return ` "
|
|
455
|
+
export function generateEndpointsConfigJSON(result: AdapterSetupResult): string {
|
|
456
|
+
if (result.endpoints.length === 0) return '';
|
|
457
|
+
return ` "endpoints": ${JSON.stringify(result.endpoints, null, 4).replace(/^/gm, ' ').trimStart()},`;
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
/**
|
|
@@ -462,21 +462,21 @@ export function generateBotsConfigJSON(result: AdapterSetupResult): string {
|
|
|
462
462
|
*/
|
|
463
463
|
export function getAdapterSetupNotes(result: AdapterSetupResult): string[] {
|
|
464
464
|
const notes: string[] = [];
|
|
465
|
-
for (const
|
|
466
|
-
const context =
|
|
467
|
-
if (context === 'telegram' &&
|
|
465
|
+
for (const entry of result.endpoints) {
|
|
466
|
+
const context = entry.context as string;
|
|
467
|
+
if (context === 'telegram' && entry.polling === false) {
|
|
468
468
|
notes.push('Telegram Webhook: 确保 HTTPS 域名可从公网访问,且 webhook.port 已放行');
|
|
469
469
|
}
|
|
470
|
-
if (context === 'telegram' &&
|
|
470
|
+
if (context === 'telegram' && entry.polling !== false) {
|
|
471
471
|
notes.push('Telegram polling: 本地开发可直接 pnpm dev,无需公网');
|
|
472
472
|
}
|
|
473
473
|
if (context === 'github') {
|
|
474
|
-
if (
|
|
474
|
+
if (entry.webhook_secret) {
|
|
475
475
|
notes.push('GitHub Webhook: App 设置 URL 为 https://<域名>/pub/github/webhook');
|
|
476
476
|
} else {
|
|
477
477
|
notes.push('GitHub polling: 无 Webhook 时将轮询 Events API(默认 60s)');
|
|
478
478
|
}
|
|
479
|
-
if (
|
|
479
|
+
if (entry.app_id) {
|
|
480
480
|
notes.push('GitHub App: 私钥路径需存在,且 App 已安装到目标仓库');
|
|
481
481
|
} else {
|
|
482
482
|
notes.push('GitHub gh CLI: 运行 gh auth login 完成认证');
|
package/src/ai.ts
CHANGED
|
@@ -76,19 +76,22 @@ const PROVIDERS = [
|
|
|
76
76
|
},
|
|
77
77
|
] as const;
|
|
78
78
|
|
|
79
|
-
function
|
|
79
|
+
function providerSdkFor(name: string): string {
|
|
80
80
|
switch (name) {
|
|
81
81
|
case 'anthropic':
|
|
82
|
-
return 'anthropic
|
|
82
|
+
return 'anthropic';
|
|
83
83
|
case 'google':
|
|
84
84
|
case 'gemini':
|
|
85
|
-
return 'google
|
|
85
|
+
return 'google';
|
|
86
86
|
case 'ollama':
|
|
87
|
-
return 'ollama
|
|
88
|
-
case '
|
|
89
|
-
return '
|
|
87
|
+
return 'ollama';
|
|
88
|
+
case 'deepseek':
|
|
89
|
+
return 'deepseek';
|
|
90
|
+
case 'moonshot':
|
|
91
|
+
case 'zhipu':
|
|
92
|
+
return 'openai-compatible';
|
|
90
93
|
default:
|
|
91
|
-
return 'openai
|
|
94
|
+
return 'openai';
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -127,7 +130,7 @@ export const RECOMMENDED_AI_DEFAULTS = {
|
|
|
127
130
|
export async function configureAI(): Promise<AISetupConfig> {
|
|
128
131
|
console.log('');
|
|
129
132
|
console.log(chalk.blue('🤖 配置 AI 智能体'));
|
|
130
|
-
console.log(chalk.gray('
|
|
133
|
+
console.log(chalk.gray(' 启用后将自动写入 package.json:@zhin.js/agent、zod、ai、所选 @ai-sdk/*、@modelcontextprotocol/sdk。'));
|
|
131
134
|
console.log(chalk.gray(' 会话持久化默认开启,将自动配置 SQLite 数据库(若尚未选择)。'));
|
|
132
135
|
|
|
133
136
|
const { enableAI } = await inquirer.prompt([
|
|
@@ -223,7 +226,7 @@ export async function configureAI(): Promise<AISetupConfig> {
|
|
|
223
226
|
|
|
224
227
|
console.log('');
|
|
225
228
|
console.log(chalk.blue('⚡ 配置 AI 触发方式'));
|
|
226
|
-
console.log(chalk.gray(' 决定
|
|
229
|
+
console.log(chalk.gray(' 决定 Endpoint 何时自动调用大模型;命令(/ 开头)不会被 AI 拦截。'));
|
|
227
230
|
|
|
228
231
|
const triggerConfig = await inquirer.prompt([
|
|
229
232
|
{
|
|
@@ -348,7 +351,7 @@ export function generateAIConfigYaml(config: AISetupConfig): string {
|
|
|
348
351
|
if (config.providers) {
|
|
349
352
|
for (const [name, providerConfig] of Object.entries(config.providers)) {
|
|
350
353
|
lines.push(` ${name}:`);
|
|
351
|
-
lines.push(`
|
|
354
|
+
lines.push(` sdk: ${providerSdkFor(name)}`);
|
|
352
355
|
if (providerConfig.apiKey) {
|
|
353
356
|
lines.push(` apiKey: ${providerConfig.apiKey}`);
|
|
354
357
|
}
|
package/src/apply.ts
CHANGED
|
@@ -30,9 +30,9 @@ export function applyAdaptersToConfig(config: Record<string, unknown>, result: A
|
|
|
30
30
|
}
|
|
31
31
|
config.plugins = plugins;
|
|
32
32
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
config.
|
|
33
|
+
const endpoints = Array.isArray(config.endpoints) ? [...config.endpoints as Record<string, unknown>[]] : [];
|
|
34
|
+
endpoints.push(...result.endpoints);
|
|
35
|
+
config.endpoints = endpoints;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function applyAIToConfig(config: Record<string, unknown>, ai: AISetupConfig): void {
|
package/src/index.ts
CHANGED
|
@@ -5,9 +5,9 @@ export type { AdapterSetupResult } from './adapter.js';
|
|
|
5
5
|
export {
|
|
6
6
|
configureAdapters,
|
|
7
7
|
generateAdapterEnvVars,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
generateEndpointsConfigYaml,
|
|
9
|
+
generateEndpointsConfigJSON,
|
|
10
|
+
generateEndpointsConfigToml,
|
|
11
11
|
getAdapterDependencies,
|
|
12
12
|
getAdapterSetupNotes,
|
|
13
13
|
} from './adapter.js';
|
|
@@ -34,6 +34,13 @@ export { configureDatabaseOptions } from './database.js';
|
|
|
34
34
|
export {
|
|
35
35
|
MCP_SDK_VERSION,
|
|
36
36
|
getAIDependencies,
|
|
37
|
+
listAIDependencyNames,
|
|
38
|
+
formatAIDependencyHint,
|
|
39
|
+
isAiEnabledInConfig,
|
|
40
|
+
resolveDefaultProviderFromConfig,
|
|
41
|
+
getRequiredAIDependenciesForConfig,
|
|
42
|
+
diagnoseAIDependencies,
|
|
43
|
+
formatAIDependencyFixCommand,
|
|
37
44
|
ensureDatabaseForAI,
|
|
38
45
|
ensureDatabaseForAdapters,
|
|
39
46
|
} from './project-deps.js';
|
package/src/project-deps.ts
CHANGED
|
@@ -1,17 +1,194 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'node:path';
|
|
1
4
|
import type { AISetupConfig } from './ai.js';
|
|
2
5
|
import type { InitOptions } from './types.js';
|
|
3
6
|
|
|
4
7
|
/** 与 monorepo 中 agent / test-bot 对齐的 MCP SDK 版本 */
|
|
5
8
|
export const MCP_SDK_VERSION = '^1.29.0';
|
|
6
9
|
|
|
10
|
+
const AI_STACK_VERSION = '^4.0.0';
|
|
11
|
+
const ZOD_VERSION = '^4.0.0';
|
|
12
|
+
const VERCEL_AI_VERSION = '^6.0.0';
|
|
13
|
+
|
|
14
|
+
const PROVIDER_SDK_PACKAGES: Record<string, { pkg: string; version: string }> = {
|
|
15
|
+
openai: { pkg: '@ai-sdk/openai', version: '^3.0.0' },
|
|
16
|
+
anthropic: { pkg: '@ai-sdk/anthropic', version: '^3.0.0' },
|
|
17
|
+
google: { pkg: '@ai-sdk/google', version: '^3.0.0' },
|
|
18
|
+
gemini: { pkg: '@ai-sdk/google', version: '^3.0.0' },
|
|
19
|
+
deepseek: { pkg: '@ai-sdk/deepseek', version: '^1.0.0' },
|
|
20
|
+
ollama: { pkg: '@ai-sdk/openai-compatible', version: '^1.0.0' },
|
|
21
|
+
moonshot: { pkg: '@ai-sdk/openai-compatible', version: '^1.0.0' },
|
|
22
|
+
zhipu: { pkg: '@ai-sdk/openai-compatible', version: '^1.0.0' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function providerSdkPackage(provider?: string): { pkg: string; version: string } | undefined {
|
|
26
|
+
if (!provider) return undefined;
|
|
27
|
+
return PROVIDER_SDK_PACKAGES[provider];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 启用 AI 时将写入 package.json 的依赖名(用于向导提示与 CLI 输出) */
|
|
31
|
+
export function listAIDependencyNames(provider?: string, options?: { includeMcp?: boolean }): string[] {
|
|
32
|
+
const names = ['@zhin.js/agent', 'zod', 'ai'];
|
|
33
|
+
if (options?.includeMcp !== false) {
|
|
34
|
+
names.push('@modelcontextprotocol/sdk');
|
|
35
|
+
}
|
|
36
|
+
const sdk = providerSdkPackage(provider);
|
|
37
|
+
if (sdk && !names.includes(sdk.pkg)) {
|
|
38
|
+
names.push(sdk.pkg);
|
|
39
|
+
}
|
|
40
|
+
return names;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatAIDependencyHint(provider?: string, options?: { includeMcp?: boolean }): string {
|
|
44
|
+
return listAIDependencyNames(provider, options).join(', ');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PackageJsonLike {
|
|
48
|
+
dependencies?: Record<string, string>;
|
|
49
|
+
devDependencies?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** zhin.config 中是否启用了 AI(enabled 未显式 false 且存在 agents/providers) */
|
|
53
|
+
export function isAiEnabledInConfig(config: { ai?: unknown }): boolean {
|
|
54
|
+
const ai = config.ai;
|
|
55
|
+
if (!ai || typeof ai !== 'object' || Array.isArray(ai)) return false;
|
|
56
|
+
const rec = ai as Record<string, unknown>;
|
|
57
|
+
if (rec.enabled === false) return false;
|
|
58
|
+
if (rec.enabled === true) return true;
|
|
59
|
+
const agents = rec.agents;
|
|
60
|
+
if (agents && typeof agents === 'object' && !Array.isArray(agents) && Object.keys(agents).length > 0) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const providers = rec.providers;
|
|
64
|
+
if (providers && typeof providers === 'object' && !Array.isArray(providers) && Object.keys(providers).length > 0) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveDefaultProviderFromConfig(config: { ai?: unknown }): string | undefined {
|
|
71
|
+
const ai = config.ai;
|
|
72
|
+
if (!ai || typeof ai !== 'object' || Array.isArray(ai)) return undefined;
|
|
73
|
+
const rec = ai as Record<string, unknown>;
|
|
74
|
+
const agents = rec.agents;
|
|
75
|
+
if (agents && typeof agents === 'object' && !Array.isArray(agents)) {
|
|
76
|
+
const zhin = (agents as Record<string, { provider?: string }>).zhin;
|
|
77
|
+
if (zhin?.provider) return String(zhin.provider);
|
|
78
|
+
const firstAgent = Object.values(agents as Record<string, { provider?: string }>)[0];
|
|
79
|
+
if (firstAgent?.provider) return String(firstAgent.provider);
|
|
80
|
+
}
|
|
81
|
+
const providers = rec.providers;
|
|
82
|
+
if (providers && typeof providers === 'object' && !Array.isArray(providers)) {
|
|
83
|
+
const first = Object.keys(providers as Record<string, unknown>)[0];
|
|
84
|
+
if (first) return first;
|
|
85
|
+
}
|
|
86
|
+
if (rec.defaultProvider != null) return String(rec.defaultProvider);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function aiConfigNeedsMcp(ai: Record<string, unknown>): boolean {
|
|
91
|
+
if (ai.memoryMcp === true) return true;
|
|
92
|
+
const servers = ai.mcpServers;
|
|
93
|
+
return Array.isArray(servers) && servers.length > 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 根据 zhin.config 的 ai 段推导应安装的 production 依赖 */
|
|
97
|
+
export function getRequiredAIDependenciesForConfig(config: { ai?: unknown }): Record<string, string> {
|
|
98
|
+
if (!isAiEnabledInConfig(config)) return {};
|
|
99
|
+
const ai = config.ai as Record<string, unknown>;
|
|
100
|
+
const setup: AISetupConfig = {
|
|
101
|
+
enabled: true,
|
|
102
|
+
defaultProvider: resolveDefaultProviderFromConfig(config),
|
|
103
|
+
memoryMcp: aiConfigNeedsMcp(ai),
|
|
104
|
+
};
|
|
105
|
+
const deps = getAIDependencies(setup);
|
|
106
|
+
if (!aiConfigNeedsMcp(ai)) {
|
|
107
|
+
delete deps['@modelcontextprotocol/sdk'];
|
|
108
|
+
}
|
|
109
|
+
return deps;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function findMissingPackageDependencies(
|
|
113
|
+
pkg: PackageJsonLike,
|
|
114
|
+
required: Record<string, string>,
|
|
115
|
+
): string[] {
|
|
116
|
+
const declared = new Set([
|
|
117
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
118
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
119
|
+
]);
|
|
120
|
+
return Object.keys(required).filter((name) => !declared.has(name));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function findUnresolvedPackageInstalls(cwd: string, packageNames: string[]): string[] {
|
|
124
|
+
const pkgJson = path.join(cwd, 'package.json');
|
|
125
|
+
if (!fs.existsSync(pkgJson)) return [...packageNames];
|
|
126
|
+
const req = createRequire(pkgJson);
|
|
127
|
+
const missing: string[] = [];
|
|
128
|
+
for (const name of packageNames) {
|
|
129
|
+
try {
|
|
130
|
+
req.resolve(`${name}/package.json`);
|
|
131
|
+
} catch {
|
|
132
|
+
missing.push(name);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return missing;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface AIDependencyDiagnosis {
|
|
139
|
+
enabled: true;
|
|
140
|
+
provider?: string;
|
|
141
|
+
required: Record<string, string>;
|
|
142
|
+
missingFromPackageJson: string[];
|
|
143
|
+
notInstalled: string[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function diagnoseAIDependencies(
|
|
147
|
+
cwd: string,
|
|
148
|
+
config: { ai?: unknown },
|
|
149
|
+
pkg?: PackageJsonLike,
|
|
150
|
+
): AIDependencyDiagnosis | null {
|
|
151
|
+
if (!isAiEnabledInConfig(config)) return null;
|
|
152
|
+
const required = getRequiredAIDependenciesForConfig(config);
|
|
153
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
154
|
+
const packageJson = pkg ?? (fs.existsSync(packageJsonPath) ? fs.readJsonSync(packageJsonPath) as PackageJsonLike : {});
|
|
155
|
+
const missingFromPackageJson = findMissingPackageDependencies(packageJson, required);
|
|
156
|
+
const declared = Object.keys(required).filter((name) => !missingFromPackageJson.includes(name));
|
|
157
|
+
const notInstalled = findUnresolvedPackageInstalls(cwd, declared);
|
|
158
|
+
return {
|
|
159
|
+
enabled: true,
|
|
160
|
+
provider: resolveDefaultProviderFromConfig(config),
|
|
161
|
+
required,
|
|
162
|
+
missingFromPackageJson,
|
|
163
|
+
notInstalled,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function formatAIDependencyFixCommand(missing: string[], required: Record<string, string>): string {
|
|
168
|
+
if (missing.length === 0) return 'pnpm install';
|
|
169
|
+
const specs = missing.map((name) => `${name}@${required[name] ?? 'latest'}`);
|
|
170
|
+
return `pnpm add ${specs.join(' ')}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
7
173
|
/**
|
|
8
|
-
* AI
|
|
174
|
+
* AI 启用时安装 agent 栈 + 所选 provider 的 @ai-sdk/* + 可选 MCP SDK。
|
|
9
175
|
*/
|
|
10
176
|
export function getAIDependencies(ai?: AISetupConfig): Record<string, string> {
|
|
11
177
|
if (!ai?.enabled) return {};
|
|
12
|
-
|
|
178
|
+
|
|
179
|
+
const deps: Record<string, string> = {
|
|
180
|
+
'@zhin.js/agent': AI_STACK_VERSION,
|
|
181
|
+
zod: ZOD_VERSION,
|
|
182
|
+
ai: VERCEL_AI_VERSION,
|
|
13
183
|
'@modelcontextprotocol/sdk': MCP_SDK_VERSION,
|
|
14
184
|
};
|
|
185
|
+
|
|
186
|
+
const sdk = providerSdkPackage(ai.defaultProvider);
|
|
187
|
+
if (sdk) {
|
|
188
|
+
deps[sdk.pkg] = sdk.version;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return deps;
|
|
15
192
|
}
|
|
16
193
|
|
|
17
194
|
/**
|