@zhin.js/scaffold-wizard 0.1.8 → 0.2.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/src/apply.ts CHANGED
@@ -1,11 +1,23 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
+ import yaml from 'yaml';
4
+ import { stringify as stringifyToml } from 'smol-toml';
3
5
  import type { AdapterSetupResult, AISetupConfig, DatabaseConfig, InitOptions } from './types.js';
4
- import { generateAdapterEnvVars, getAdapterDependencies } from './adapter.js';
6
+ import {
7
+ collectAdapterPluginConfigs,
8
+ collectAdapterPluginManifest,
9
+ generateAdapterEnvVars,
10
+ getAdapterDependencies,
11
+ } from './adapter.js';
5
12
  import { generateAIEnvVars, materializeAIConfig } from './ai.js';
6
13
  import { ensureDatabaseForAdapters, ensureDatabaseForAI, getAIDependencies } from './project-deps.js';
14
+ import { DEFAULT_CREATE_BOT_HTTP_PORT } from './zhin-stack-deps.js';
15
+ import { packageToInstanceKey } from './project-config-plan.js';
7
16
 
8
- /** Host 插件(Webhook / Console 必需) */
17
+ /**
18
+ * @deprecated Plugin Runtime 由 CLI composition root 装配 Console/HTTP Host,
19
+ * 不再需要在 plugins 清单中声明 host 插件;保留导出仅为兼容。
20
+ */
9
21
  export const CONSOLE_HOST_PLUGINS = [
10
22
  '@zhin.js/host-router',
11
23
  '@zhin.js/host-api',
@@ -13,32 +25,67 @@ export const CONSOLE_HOST_PLUGINS = [
13
25
 
14
26
  export function applyDatabaseToConfig(config: Record<string, unknown>, database: DatabaseConfig): void {
15
27
  config.database = database;
16
- if (database.dialect === 'sqlite') {
17
- config.inbox = { enabled: true };
28
+ }
29
+
30
+ /** 将 legacy 数组形式 plugins 归一化为新 runtime 的 instanceKey 映射 */
31
+ export function normalizePluginsMap(value: unknown): Record<string, unknown> {
32
+ if (Array.isArray(value)) {
33
+ const map: Record<string, unknown> = {};
34
+ for (const entry of value) {
35
+ if (typeof entry !== 'string') continue;
36
+ map[packageToInstanceKey(entry)] ??= {};
37
+ }
38
+ return map;
18
39
  }
40
+ if (value && typeof value === 'object') {
41
+ return { ...(value as Record<string, unknown>) };
42
+ }
43
+ return {};
19
44
  }
20
45
 
21
46
  export function applyAdaptersToConfig(config: Record<string, unknown>, result: AdapterSetupResult): void {
22
- const plugins = Array.isArray(config.plugins) ? [...config.plugins as string[]] : [];
23
- for (const plugin of result.plugins) {
24
- if (!plugins.includes(plugin)) plugins.push(plugin);
25
- }
26
- if (result.plugins.some((p: string) => p !== '@zhin.js/adapter-sandbox')) {
27
- for (const hostPlugin of CONSOLE_HOST_PLUGINS) {
28
- if (!plugins.includes(hostPlugin)) plugins.push(hostPlugin);
29
- }
47
+ const plugins = normalizePluginsMap(config.plugins);
48
+ for (const instance of result.instances) {
49
+ plugins[instance.instanceKey] = instance.config;
30
50
  }
31
51
  config.plugins = plugins;
32
-
33
- const endpoints = Array.isArray(config.endpoints) ? [...config.endpoints as Record<string, unknown>[]] : [];
34
- endpoints.push(...result.endpoints);
35
- config.endpoints = endpoints;
52
+ // legacy endpoints 列表不被 runtime 配置 schema 接受(additionalProperties: false)
53
+ delete config.endpoints;
36
54
  }
37
55
 
38
56
  export function applyAIToConfig(config: Record<string, unknown>, ai: AISetupConfig): void {
39
57
  config.ai = materializeAIConfig(ai);
40
58
  }
41
59
 
60
+ /** 把 zhin.plugins 清单条目合并进项目 package.json(zhin setup 路径) */
61
+ export async function mergePluginManifestIntoPackageJson(
62
+ projectDir: string,
63
+ entries: Array<{ package: string; instanceKey: string }>,
64
+ ): Promise<boolean> {
65
+ if (entries.length === 0) return false;
66
+ const pkgPath = path.join(projectDir, 'package.json');
67
+ if (!(await fs.pathExists(pkgPath))) return false;
68
+
69
+ const pkg = await fs.readJson(pkgPath);
70
+ const zhin = pkg.zhin && typeof pkg.zhin === 'object' && !Array.isArray(pkg.zhin)
71
+ ? pkg.zhin
72
+ : { protocol: 1, type: 'plugin', entry: './plugin.ts' };
73
+ const manifest: Array<{ package: string; instanceKey: string }> = Array.isArray(zhin.plugins)
74
+ ? [...zhin.plugins]
75
+ : [];
76
+ let changed = false;
77
+ for (const entry of entries) {
78
+ if (manifest.some((item) => item?.instanceKey === entry.instanceKey)) continue;
79
+ manifest.push({ package: entry.package, instanceKey: entry.instanceKey });
80
+ changed = true;
81
+ }
82
+ if (!changed && pkg.zhin) return false;
83
+ zhin.plugins = manifest;
84
+ pkg.zhin = zhin;
85
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
86
+ return true;
87
+ }
88
+
42
89
  export async function appendWizardEnvVars(
43
90
  projectDir: string,
44
91
  adapters?: AdapterSetupResult,
@@ -47,11 +94,16 @@ export async function appendWizardEnvVars(
47
94
  const extra =
48
95
  (adapters ? generateAdapterEnvVars(adapters) : '') +
49
96
  (ai ? generateAIEnvVars(ai) : '');
50
- if (!extra) return;
97
+ if (extra) {
98
+ const envPath = path.join(projectDir, '.env');
99
+ const existing = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, 'utf-8') : '';
100
+ await fs.writeFile(envPath, existing.replace(/\s*$/, '') + extra + '\n');
101
+ }
51
102
 
52
- const envPath = path.join(projectDir, '.env');
53
- const existing = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, 'utf-8') : '';
54
- await fs.writeFile(envPath, existing.replace(/\s*$/, '') + extra + '\n');
103
+ // 新 Plugin Runtime 通过 package.json zhin.plugins 清单挂载子插件,与依赖一起落盘
104
+ if (adapters) {
105
+ await mergePluginManifestIntoPackageJson(projectDir, collectAdapterPluginManifest(adapters));
106
+ }
55
107
  }
56
108
 
57
109
  export function collectWizardDependencies(
@@ -95,5 +147,102 @@ export function finalizeWizardOptions(options: InitOptions): void {
95
147
  export function applyWizardOptionsToConfig(config: Record<string, unknown>, options: InitOptions): void {
96
148
  if (options.database) applyDatabaseToConfig(config, options.database);
97
149
  if (options.adapters) applyAdaptersToConfig(config, options.adapters);
98
- if (options.ai) applyAIToConfig(config, options.ai);
150
+ if (options.ai?.enabled) applyAIToConfig(config, options.ai);
151
+ }
152
+
153
+ /**
154
+ * 数据库配置物化为可落盘对象;非 SQLite 连接参数引用 .env(${VAR} 由 runtime 展开)
155
+ */
156
+ export function materializeDatabaseConfig(config: DatabaseConfig): Record<string, unknown> {
157
+ switch (config.dialect) {
158
+ case 'mysql':
159
+ return {
160
+ dialect: 'mysql',
161
+ host: '${DB_HOST}',
162
+ port: '${DB_PORT}',
163
+ user: '${DB_USER}',
164
+ password: '${DB_PASSWORD}',
165
+ database: '${DB_DATABASE}',
166
+ };
167
+ case 'pg':
168
+ return {
169
+ dialect: 'pg',
170
+ host: '${DB_HOST}',
171
+ port: '${DB_PORT}',
172
+ user: '${DB_USER}',
173
+ password: '${DB_PASSWORD}',
174
+ database: '${DB_DATABASE}',
175
+ };
176
+ case 'mongodb':
177
+ return { dialect: 'mongodb', url: '${DB_URL}', dbName: '${DB_NAME}' };
178
+ case 'redis':
179
+ return {
180
+ dialect: 'redis',
181
+ socket: { host: '${REDIS_HOST}', port: '${REDIS_PORT}' },
182
+ password: '${REDIS_PASSWORD}',
183
+ database: '${REDIS_DB}',
184
+ };
185
+ case 'sqlite':
186
+ default: {
187
+ const sqlite: Record<string, unknown> = { dialect: 'sqlite' };
188
+ if (config.filename) sqlite.filename = config.filename;
189
+ if (config.mode) sqlite.mode = config.mode;
190
+ return sqlite;
191
+ }
192
+ }
193
+ }
194
+
195
+ /** 生活助手模板的额外 AI 配置(知识库 + compaction + 执行审批) */
196
+ function applyLifeAssistantAiExtras(ai: Record<string, unknown>): Record<string, unknown> {
197
+ const agent = ai.agent && typeof ai.agent === 'object' && !Array.isArray(ai.agent)
198
+ ? { ...(ai.agent as Record<string, unknown>) }
199
+ : {};
200
+ return {
201
+ ...ai,
202
+ knowledge: { baseDir: 'knowledge' },
203
+ agent: {
204
+ ...agent,
205
+ execSecurity: 'allowlist',
206
+ execApprovalMode: 'ask',
207
+ compaction: { enabled: true, auto: true, keepRecentTokens: 20000 },
208
+ },
209
+ };
210
+ }
211
+
212
+ /**
213
+ * 构建新 Plugin Runtime 配置文档(顶层 http/database/ai + plugins.<instanceKey>)。
214
+ * 顶层键与 packages/im/runtime/src/config-composer.ts 的 effectiveSchema 对齐。
215
+ */
216
+ export function buildRuntimeConfigDocument(options: InitOptions): Record<string, unknown> {
217
+ const doc: Record<string, unknown> = {};
218
+
219
+ if (options.database) {
220
+ doc.database = materializeDatabaseConfig(options.database);
221
+ }
222
+
223
+ doc.http = {
224
+ token: '${HTTP_TOKEN}',
225
+ port: DEFAULT_CREATE_BOT_HTTP_PORT,
226
+ base: '/api',
227
+ corsOrigins: ['https://console.zhin.dev'],
228
+ };
229
+
230
+ if (options.ai?.enabled) {
231
+ const ai = materializeAIConfig(options.ai);
232
+ doc.ai = options.template === 'life-assistant' ? applyLifeAssistantAiExtras(ai) : ai;
233
+ }
234
+
235
+ doc.plugins = options.adapters ? collectAdapterPluginConfigs(options.adapters) : {};
236
+
237
+ return doc;
238
+ }
239
+
240
+ /** 序列化配置文档为 zhin.config.yml / json / toml 文本 */
241
+ export function serializeRuntimeConfig(
242
+ doc: Record<string, unknown>,
243
+ format: 'yaml' | 'json' | 'toml',
244
+ ): string {
245
+ if (format === 'json') return `${JSON.stringify(doc, null, 2)}\n`;
246
+ if (format === 'toml') return stringifyToml(doc);
247
+ return yaml.stringify(doc);
99
248
  }
package/src/index.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  export type { InitOptions, DatabaseConfig } from './types.js';
2
2
  export { DATABASE_PACKAGES } from './types.js';
3
3
 
4
- export type { AdapterSetupResult } from './adapter.js';
4
+ export type { AdapterSetupResult, AdapterPluginInstance } from './adapter.js';
5
5
  export {
6
6
  configureAdapters,
7
+ buildFieldBasedInstanceConfig,
7
8
  generateAdapterEnvVars,
8
- generateEndpointsConfigYaml,
9
- generateEndpointsConfigJSON,
10
- generateEndpointsConfigToml,
9
+ collectAdapterPluginConfigs,
10
+ collectAdapterPluginManifest,
11
11
  getAdapterDependencies,
12
12
  getAdapterSetupNotes,
13
13
  } from './adapter.js';
@@ -89,6 +89,7 @@ export {
89
89
  migrateAiLegacyConfig,
90
90
  diagnoseConsoleConfig,
91
91
  applyConsoleConfigFixes,
92
+ packageToInstanceKey,
92
93
  } from './project-config-plan.js';
93
94
  export type {
94
95
  LoadedProjectConfig,
@@ -106,6 +107,11 @@ export {
106
107
  appendWizardEnvVars,
107
108
  collectWizardDependencies,
108
109
  mergeDependenciesIntoPackageJson,
110
+ mergePluginManifestIntoPackageJson,
109
111
  finalizeWizardOptions,
110
112
  applyWizardOptionsToConfig,
113
+ normalizePluginsMap,
114
+ materializeDatabaseConfig,
115
+ buildRuntimeConfigDocument,
116
+ serializeRuntimeConfig,
111
117
  } from './apply.js';
@@ -12,10 +12,19 @@ const CONFIG_CANDIDATES = [
12
12
  'zhin.config.toml',
13
13
  'zhin.config.ts',
14
14
  ] as const;
15
+ /** @deprecated Plugin Runtime 由 CLI 装配 Console Host,无需在配置中声明;仅用于 legacy 数组配置诊断 */
15
16
  const CONSOLE_HOST_PLUGINS = ['@zhin.js/host-router', '@zhin.js/host-api'] as const;
16
17
  const SANDBOX_PLUGIN = '@zhin.js/adapter-sandbox';
17
18
  const LEGACY_AI_PROVIDER_FIELDS = ['driver', 'api', 'preset', 'spec'] as const;
18
19
 
20
+ /** 由插件包名推导默认 instanceKey(@zhin.js/adapter-telegram → telegram) */
21
+ export function packageToInstanceKey(packageName: string): string {
22
+ const name = packageName.includes('/')
23
+ ? packageName.split('/').pop() ?? packageName
24
+ : packageName;
25
+ return name.replace(/^(adapter|plugin|service)-/, '');
26
+ }
27
+
19
28
  export type ProjectConfigFormat = 'yaml' | 'json' | 'toml' | 'ts';
20
29
 
21
30
  export interface LoadedProjectConfig {
@@ -164,17 +173,34 @@ export function loadProjectConfig(cwd = process.cwd(), configPath?: string): Loa
164
173
  }
165
174
 
166
175
  function ensurePlugins(config: Record<string, unknown>, pluginsToAdd: readonly string[], mutations: string[]): void {
167
- const plugins = Array.isArray(config.plugins)
168
- ? config.plugins.filter((p): p is string => typeof p === 'string')
169
- : [];
176
+ if (Array.isArray(config.plugins)) {
177
+ // legacy 数组形式:保持原形态追加,避免改写旧项目结构
178
+ const plugins = config.plugins.filter((p): p is string => typeof p === 'string');
179
+ let changed = false;
180
+ for (const plugin of pluginsToAdd) {
181
+ if (plugins.includes(plugin)) continue;
182
+ plugins.push(plugin);
183
+ mutations.push(`added ${plugin} to plugins`);
184
+ changed = true;
185
+ }
186
+ if (changed) {
187
+ config.plugins = plugins;
188
+ }
189
+ return;
190
+ }
191
+ // 新 Plugin Runtime:plugins 为 <instanceKey>: <配置> 映射
192
+ const plugins = config.plugins && typeof config.plugins === 'object'
193
+ ? { ...(config.plugins as Record<string, unknown>) }
194
+ : {};
170
195
  let changed = false;
171
- for (const plugin of pluginsToAdd) {
172
- if (plugins.includes(plugin)) continue;
173
- plugins.push(plugin);
174
- mutations.push(`added ${plugin} to plugins`);
196
+ for (const pkg of pluginsToAdd) {
197
+ const instanceKey = packageToInstanceKey(pkg);
198
+ if (instanceKey in plugins) continue;
199
+ plugins[instanceKey] = {};
200
+ mutations.push(`added ${instanceKey} (${pkg}) to plugins`);
175
201
  changed = true;
176
202
  }
177
- if (changed || !Array.isArray(config.plugins)) {
203
+ if (changed || !config.plugins || typeof config.plugins !== 'object') {
178
204
  config.plugins = plugins;
179
205
  }
180
206
  }
@@ -318,7 +344,8 @@ export function createProjectConfigPlan(options: ProjectConfigPlanOptions): Proj
318
344
  const mutations: string[] = [];
319
345
 
320
346
  if (loaded.status === 'loaded' || options.config) {
321
- if (options.ensureConsole) {
347
+ if (options.ensureConsole && Array.isArray(after.plugins)) {
348
+ // legacy 数组形态:host 插件仍需写入列表(旧项目行为不变)
322
349
  ensurePlugins(after, CONSOLE_HOST_PLUGINS, mutations);
323
350
  }
324
351
  if (options.ensureSandbox) {
@@ -327,7 +354,8 @@ export function createProjectConfigPlan(options: ProjectConfigPlanOptions): Proj
327
354
  if (options.enablePlugins?.length) {
328
355
  ensurePlugins(after, options.enablePlugins, mutations);
329
356
  }
330
- if (options.ensureHttp) {
357
+ if (options.ensureConsole || options.ensureHttp) {
358
+ // 新形态下 Console Host 由 CLI composition root 装配;Console 可达只需 http.token + corsOrigins
331
359
  ensureHttp(after, mutations);
332
360
  }
333
361
  if (options.migrateAiLegacy) {
@@ -369,17 +397,29 @@ export async function applyProjectConfigPlan(plan: ProjectConfigPlan): Promise<b
369
397
  }
370
398
 
371
399
  export function diagnoseConsoleConfig(config: Record<string, unknown>): ConsoleConfigDiagnosis {
372
- const plugins = Array.isArray(config.plugins)
373
- ? config.plugins.filter((p): p is string => typeof p === 'string')
374
- : [];
375
400
  const http = config.http && typeof config.http === 'object' && !Array.isArray(config.http)
376
401
  ? config.http as Record<string, unknown>
377
402
  : {};
378
403
  const corsOrigins = Array.isArray(http.corsOrigins) ? http.corsOrigins : [];
379
404
 
405
+ if (Array.isArray(config.plugins)) {
406
+ // legacy 数组形式配置
407
+ const plugins = config.plugins.filter((p): p is string => typeof p === 'string');
408
+ return {
409
+ missingHostPlugins: CONSOLE_HOST_PLUGINS.filter((plugin) => !plugins.includes(plugin)),
410
+ missingSandboxPlugin: !plugins.includes(SANDBOX_PLUGIN),
411
+ missingConsoleOrigin: !hasConsoleOrigin(corsOrigins.filter((origin): origin is string => typeof origin === 'string')),
412
+ missingHttpToken: typeof http.token !== 'string' || http.token.trim().length === 0,
413
+ };
414
+ }
415
+
416
+ // 新 Plugin Runtime:Console Host 由 CLI 装配,无需 host 插件;Sandbox 为 plugins.sandbox 实例
417
+ const plugins = config.plugins && typeof config.plugins === 'object'
418
+ ? config.plugins as Record<string, unknown>
419
+ : {};
380
420
  return {
381
- missingHostPlugins: CONSOLE_HOST_PLUGINS.filter((plugin) => !plugins.includes(plugin)),
382
- missingSandboxPlugin: !plugins.includes(SANDBOX_PLUGIN),
421
+ missingHostPlugins: [],
422
+ missingSandboxPlugin: !('sandbox' in plugins),
383
423
  missingConsoleOrigin: !hasConsoleOrigin(corsOrigins.filter((origin): origin is string => typeof origin === 'string')),
384
424
  missingHttpToken: typeof http.token !== 'string' || http.token.trim().length === 0,
385
425
  };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * 微信 iLink(ClawBot)扫码登录流程 — 向导内联实现。
3
+ * 协议与 plugins/adapters/weixin-ilink/src/login.ts 一致(SSOT),
4
+ * 此处复制最小 HTTP 面,避免向导在适配器包安装前产生依赖。
5
+ */
6
+
7
+ export interface IlinkQrLoginResult {
8
+ readonly botToken: string;
9
+ readonly ilinkUserId?: string;
10
+ readonly ilinkBotId?: string;
11
+ readonly baseUrl: string;
12
+ }
13
+
14
+ interface QrCodeResponse {
15
+ qrcode: string;
16
+ qrcode_img_content: string;
17
+ }
18
+
19
+ type QrStatus =
20
+ | 'wait'
21
+ | 'scaned'
22
+ | 'confirmed'
23
+ | 'expired'
24
+ | 'scaned_but_redirect'
25
+ | 'need_verifycode'
26
+ | 'verify_code_blocked'
27
+ | 'binded_redirect';
28
+
29
+ interface StatusResponse {
30
+ status: QrStatus;
31
+ bot_token?: string;
32
+ ilink_endpoint_id?: string;
33
+ baseurl?: string;
34
+ ilink_user_id?: string;
35
+ redirect_host?: string;
36
+ }
37
+
38
+ const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';
39
+ const DEFAULT_BOT_TYPE = '3';
40
+ const POLL_INTERVAL_MS = 500;
41
+ const QR_LONG_POLL_TIMEOUT_MS = 35_000;
42
+
43
+ export interface IlinkQrLoginOptions {
44
+ readonly baseUrl?: string;
45
+ readonly timeoutMs?: number;
46
+ /** 拿到二维码内容(URL)后回调,由调用方负责展示(终端 QR / 链接)。 */
47
+ readonly onQrCode: (url: string) => void | Promise<void>;
48
+ /** 扫码状态变化提示(scaned → 已扫码待确认)。 */
49
+ readonly onStatus?: (status: QrStatus) => void;
50
+ readonly fetchImpl?: typeof fetch;
51
+ readonly sleep?: (ms: number) => Promise<void>;
52
+ }
53
+
54
+ export class IlinkQrLoginError extends Error {
55
+ constructor(
56
+ message: string,
57
+ readonly reason: 'expired' | 'timeout' | 'network' | 'protocol',
58
+ ) {
59
+ super(message);
60
+ this.name = 'IlinkQrLoginError';
61
+ }
62
+ }
63
+
64
+ /** 与 adapter login.ts 的 buildBaseInfo 对齐的最小结构。 */
65
+ function buildBaseInfo(): Record<string, string> {
66
+ return { channel_version: '0.0.0', bot_agent: 'Zhin.js' };
67
+ }
68
+
69
+ /**
70
+ * 微信 iLink 扫码登录:获取二维码 → 展示 → 长轮询直至 confirmed。
71
+ * 成功返回 bot_token 等凭证;过期/超时/网络失败抛 IlinkQrLoginError。
72
+ */
73
+ export async function loginWithIlinkQr(options: IlinkQrLoginOptions): Promise<IlinkQrLoginResult> {
74
+ const fetchImpl = options.fetchImpl ?? fetch;
75
+ const sleep = options.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
76
+ const apiBaseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
77
+ const timeoutMs = options.timeoutMs ?? 480_000;
78
+
79
+ let qr: QrCodeResponse;
80
+ try {
81
+ const res = await fetchImpl(
82
+ `${apiBaseUrl}/ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(DEFAULT_BOT_TYPE)}`,
83
+ {
84
+ method: 'POST',
85
+ headers: { 'content-type': 'application/json' },
86
+ body: JSON.stringify({ local_token_list: [], base_info: buildBaseInfo() }),
87
+ },
88
+ );
89
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
90
+ qr = await res.json() as QrCodeResponse;
91
+ } catch (error) {
92
+ throw new IlinkQrLoginError(
93
+ `获取登录二维码失败:${error instanceof Error ? error.message : String(error)}`,
94
+ 'network',
95
+ );
96
+ }
97
+
98
+ const qrcodeUrl = qr.qrcode_img_content || qr.qrcode;
99
+ await options.onQrCode(qrcodeUrl);
100
+
101
+ const deadline = Date.now() + timeoutMs;
102
+ let currentBaseUrl = apiBaseUrl;
103
+ let lastStatus: QrStatus | undefined;
104
+
105
+ while (Date.now() < deadline) {
106
+ let status: StatusResponse;
107
+ try {
108
+ const res = await fetchImpl(
109
+ `${currentBaseUrl}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qr.qrcode)}`,
110
+ { signal: AbortSignal.timeout(QR_LONG_POLL_TIMEOUT_MS) },
111
+ );
112
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
113
+ status = await res.json() as StatusResponse;
114
+ } catch {
115
+ // 长轮询超时/瞬断视为继续等待
116
+ await sleep(POLL_INTERVAL_MS);
117
+ continue;
118
+ }
119
+
120
+ if (status.status !== lastStatus) {
121
+ lastStatus = status.status;
122
+ options.onStatus?.(status.status);
123
+ }
124
+
125
+ if (status.status === 'scaned_but_redirect' && status.redirect_host) {
126
+ currentBaseUrl = status.redirect_host.startsWith('http')
127
+ ? status.redirect_host
128
+ : `https://${status.redirect_host}`;
129
+ }
130
+
131
+ if (status.status === 'confirmed' || status.status === 'binded_redirect') {
132
+ if (!status.bot_token) {
133
+ throw new IlinkQrLoginError('扫码成功但未返回 bot_token,请重试或改用手动输入', 'protocol');
134
+ }
135
+ return {
136
+ botToken: status.bot_token,
137
+ ...(status.ilink_user_id ? { ilinkUserId: status.ilink_user_id } : {}),
138
+ ...(status.ilink_endpoint_id ? { ilinkBotId: status.ilink_endpoint_id } : {}),
139
+ baseUrl: status.baseurl ?? currentBaseUrl,
140
+ };
141
+ }
142
+
143
+ if (status.status === 'expired') {
144
+ throw new IlinkQrLoginError('二维码已过期', 'expired');
145
+ }
146
+
147
+ await sleep(POLL_INTERVAL_MS);
148
+ }
149
+
150
+ throw new IlinkQrLoginError('微信扫码登录超时', 'timeout');
151
+ }
@@ -6,12 +6,20 @@ import { findMissingPackageDependencies, findUnresolvedPackageInstalls, isAiEnab
6
6
 
7
7
  /**
8
8
  * User project dependency policy: scaffolded dependencies intentionally float to latest.
9
+ * WARNING: this is not reproducible; pin versions for production. See scaffold-wizard README.
9
10
  */
10
11
  export const ZHIN_STACK_VERSIONS = {
11
12
  'zhin.js': 'latest',
12
13
  '@zhin.js/cli': 'latest',
13
14
  '@zhin.js/agent': 'latest',
14
15
  '@zhin.js/database': 'latest',
16
+ '@zhin.js/plugin-runtime': 'latest',
17
+ '@zhin.js/runtime': 'latest',
18
+ '@zhin.js/adapter': 'latest',
19
+ '@zhin.js/command': 'latest',
20
+ '@zhin.js/component': 'latest',
21
+ '@zhin.js/core': 'latest',
22
+ '@zhin.js/tool': 'latest',
15
23
  '@zhin.js/host-router': 'latest',
16
24
  '@zhin.js/host-api': 'latest',
17
25
  '@zhin.js/client': 'latest',
@@ -81,10 +89,17 @@ function resolveStackVersion(packageName: string): string {
81
89
  }
82
90
 
83
91
  function collectZhinPluginsFromConfig(config: Record<string, unknown>): string[] {
84
- const plugins = Array.isArray(config.plugins)
85
- ? config.plugins.filter((p): p is string => typeof p === 'string')
86
- : [];
87
- return [...new Set(plugins.filter((p) => p.startsWith('@zhin.js/')))];
92
+ // legacy 数组形式:条目即包名
93
+ if (Array.isArray(config.plugins)) {
94
+ const plugins = config.plugins.filter((p): p is string => typeof p === 'string');
95
+ return [...new Set(plugins.filter((p) => p.startsWith('@zhin.js/')))];
96
+ }
97
+ // 新 runtime:plugins 为 instanceKey 映射,包名在 package.json zhin.plugins 清单中,
98
+ // 无法从配置推导;仅收集直接以包名出现的键(防御性)
99
+ if (config.plugins && typeof config.plugins === 'object') {
100
+ return Object.keys(config.plugins as Record<string, unknown>).filter((key) => key.startsWith('@zhin.js/'));
101
+ }
102
+ return [];
88
103
  }
89
104
 
90
105
  /** create-zhin / minimal-bot 默认 Host 端口(避免与常见 8086 占用冲突) */
@@ -100,15 +115,16 @@ export function getCreateBotPnpmConfig(_aiEnabled?: boolean): Record<string, unk
100
115
  };
101
116
  }
102
117
 
103
- /** create-zhin-app 默认 bot 骨架依赖(不含适配器 / AI / 数据库驱动) */
118
+ /** create-zhin-app Plugin Runtime 骨架 production 依赖(不含适配器 / AI / 数据库驱动) */
104
119
  export function getCreateBotBaseDependencies(): Record<string, string> {
105
120
  return {
106
121
  'zhin.js': ZHIN_STACK_VERSIONS['zhin.js'],
107
- '@zhin.js/cli': ZHIN_STACK_VERSIONS['@zhin.js/cli'],
108
- '@zhin.js/host-router': ZHIN_STACK_VERSIONS['@zhin.js/host-router'],
109
- '@zhin.js/client': ZHIN_STACK_VERSIONS['@zhin.js/client'],
110
- '@zhin.js/host-api': ZHIN_STACK_VERSIONS['@zhin.js/host-api'],
111
- '@zhin.js/contract': ZHIN_STACK_VERSIONS['@zhin.js/contract'],
122
+ '@zhin.js/plugin-runtime': ZHIN_STACK_VERSIONS['@zhin.js/plugin-runtime'],
123
+ '@zhin.js/runtime': ZHIN_STACK_VERSIONS['@zhin.js/runtime'],
124
+ '@zhin.js/adapter': ZHIN_STACK_VERSIONS['@zhin.js/adapter'],
125
+ '@zhin.js/command': ZHIN_STACK_VERSIONS['@zhin.js/command'],
126
+ '@zhin.js/component': ZHIN_STACK_VERSIONS['@zhin.js/component'],
127
+ '@zhin.js/core': ZHIN_STACK_VERSIONS['@zhin.js/core'],
112
128
  '@zhin.js/satori': ZHIN_STACK_VERSIONS['@zhin.js/satori'],
113
129
  };
114
130
  }