@ai-zen/agents-host 0.1.0-alpha.1 → 0.1.0-alpha.2

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/app.d.ts CHANGED
@@ -211,6 +211,13 @@ export declare class HostApp {
211
211
  /** R50 的那条警告只说一次(描述的是装配能力,不是某一轮的错) */
212
212
  private handoffWarningSent;
213
213
  private constructor();
214
+ /**
215
+ * 报一条「配置文件坏了、已恢复出厂」(D32)。
216
+ *
217
+ * 恢复动作本身在读到坏文件那一刻就做完了(写回 + 用出厂配置),这里只负责让用户知道
218
+ * 自己被重置过 —— warning 是 PRD F-J2 要求上通知栏的那一档。
219
+ */
220
+ private noticeRecoveredConfig;
214
221
  /**
215
222
  * 造一个 `HostApp`(不监听)。
216
223
  *
@@ -327,7 +334,7 @@ export declare class HostApp {
327
334
  private createConversationRuntime;
328
335
  /** 素材转换要的实发模型与端点(F-H7 的判定依据:模型能不能看图、端点有没有 Files API) */
329
336
  private resolveModelContext;
330
- /** 读配置;坏了就是 `config_invalid`(服务层读配置时也是这个口径) */
337
+ /** 读配置(`config.json` 坏了由 SDK 恢复出厂,D32) */
331
338
  private readConfig;
332
339
  /** 凭证校验:顺手记下「这条进程被用过」(握手成功即算) */
333
340
  private buildVerifier;
package/dist/app.js CHANGED
@@ -86,6 +86,32 @@ export function readDiskVersion() {
86
86
  return null;
87
87
  }
88
88
  // ---------------------------------------------------------------------------
89
+ // 配置文件恢复出厂的通知口
90
+ // ---------------------------------------------------------------------------
91
+ /**
92
+ * 配置文件坏了被恢复出厂时的通知口(D32)。
93
+ *
94
+ * `host.json` 与 `config.json` 的首次读都在通知服务之前(端口要在开库之前定下来,
95
+ * `bootstrap()` 在造对象之前),装配期发生的恢复先攒在这里;`HostApp` 造好之后 `attach()`
96
+ * 接上真正的通知,攒下的补发完,此后每恢复一次当场发一次。
97
+ */
98
+ class ConfigRecoverySpool {
99
+ queued = [];
100
+ sink = null;
101
+ /** 交给 `readHostSettings` / `ConfigManager` 的那个回调 */
102
+ onRecovered = (path) => {
103
+ if (this.sink !== null)
104
+ return this.sink(path);
105
+ this.queued.push(path);
106
+ };
107
+ /** 接上通知口,并把攒下的那些补发完 */
108
+ async attach(sink) {
109
+ this.sink = sink;
110
+ for (const path of this.queued.splice(0))
111
+ await sink(path);
112
+ }
113
+ }
114
+ // ---------------------------------------------------------------------------
89
115
  // HostApp
90
116
  // ---------------------------------------------------------------------------
91
117
  export class HostApp {
@@ -425,6 +451,20 @@ export class HostApp {
425
451
  });
426
452
  this.hubForPush = this.hub;
427
453
  }
454
+ /**
455
+ * 报一条「配置文件坏了、已恢复出厂」(D32)。
456
+ *
457
+ * 恢复动作本身在读到坏文件那一刻就做完了(写回 + 用出厂配置),这里只负责让用户知道
458
+ * 自己被重置过 —— warning 是 PRD F-J2 要求上通知栏的那一档。
459
+ */
460
+ async noticeRecoveredConfig(path) {
461
+ await this.noticeService.create({
462
+ level: "warning",
463
+ code: "notice.configRecovered",
464
+ params: { path },
465
+ duration: null,
466
+ });
467
+ }
428
468
  /**
429
469
  * 造一个 `HostApp`(不监听)。
430
470
  *
@@ -434,8 +474,11 @@ export class HostApp {
434
474
  */
435
475
  static async create(options = {}) {
436
476
  const paths = resolvePaths(options.aiZenDir);
477
+ // 配置文件坏了就恢复出厂(D32):恢复过的文件名先攒着,等通知服务起来再补一条 warning
478
+ const recoveries = new ConfigRecoverySpool();
437
479
  // D25 的中间那一档:只在这一档真的要被用到时才读(参数给了就不必读,坏文件也不该拦住显式端口)
438
- const configPort = options.configPort ?? (options.port === undefined ? (await readHostSettings(paths)).port : undefined);
480
+ const configPort = options.configPort ??
481
+ (options.port === undefined ? (await readHostSettings(paths, recoveries.onRecovered)).port : undefined);
439
482
  await mkdir(paths.dataRoot, { recursive: true });
440
483
  await mkdir(paths.runDir, { recursive: true });
441
484
  await mkdir(paths.assetsDir, { recursive: true });
@@ -447,13 +490,14 @@ export class HostApp {
447
490
  await db.close();
448
491
  throw error;
449
492
  }
450
- const configManager = new ConfigManager(paths.configFile);
493
+ const configManager = new ConfigManager(paths.configFile, recoveries.onRecovered);
451
494
  // SDK 口径的首次初始化:目录 + config.json + 默认助手 + 子 Agent + mcp.json + 出厂 Skill
452
495
  // + 思考策略表。已有文件不覆盖(用户自定义的助手定义 `custom: true` 原样保留)。
453
496
  await configManager.bootstrap();
454
497
  const logging = resolveLogger(options, paths);
498
+ let app;
455
499
  try {
456
- return new HostApp(options, paths, db, configManager, configPort, logging.logger);
500
+ app = new HostApp(options, paths, db, configManager, configPort, logging.logger);
457
501
  }
458
502
  catch (error) {
459
503
  // 构造途中的抛出(端口不合法、接线造不出来)也要把库关掉:worker 线程不该被漏下
@@ -461,6 +505,9 @@ export class HostApp {
461
505
  await logging.own?.close();
462
506
  throw error;
463
507
  }
508
+ // 通知服务就位了:装配期恢复过出厂的那几份配置各补一条 warning
509
+ await recoveries.attach((path) => app.noticeRecoveredConfig(path));
510
+ return app;
464
511
  }
465
512
  // -------------------------------------------------------------------------
466
513
  // 启停
@@ -919,7 +966,7 @@ export class HostApp {
919
966
  : undefined;
920
967
  return { model: model ?? null, endpoint: endpoint ?? null };
921
968
  }
922
- /** 读配置;坏了就是 `config_invalid`(服务层读配置时也是这个口径) */
969
+ /** 读配置(`config.json` 坏了由 SDK 恢复出厂,D32) */
923
970
  async readConfig() {
924
971
  return await this.configManager.read();
925
972
  }
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ConfigRecoveredHandler } from "@ai-zen/agents-sdk";
1
2
  /**
2
3
  * 路径与监听的来源(plan §1 的 `config.ts` 契约)。
3
4
  *
@@ -90,8 +91,9 @@ export interface HostSettings {
90
91
  /**
91
92
  * 读 `<dataRoot>/host.json`。
92
93
  *
93
- * 三种情况分开:文件不在 → 空设置(这一档没说话);读不出来 / 不是 JSON 对象 → `config_invalid`
94
- * (写坏了要让人看见,不能当没有,否则用户改完没有任何反馈);`port` 在但不是可用端口 →
94
+ * 三种情况分开:文件不在 → 空设置(这一档没说话,静默);读不出来 / 不是 JSON / 不是对象 →
95
+ * 就是坏文件,恢复出厂:当空设置用(默认端口、默认语言)、写回一份合法的 `{}`、回调一次
96
+ * (D32:坏了就恢复出厂,下次启动不再遇到同一个坏文件);`port` 在但不是可用端口 →
95
97
  * `config_invalid`(裁定 R67:显式给出的非法值报错,不静默回落)。
96
98
  */
97
- export declare function readHostSettings(paths: HostPaths): Promise<HostSettings>;
99
+ export declare function readHostSettings(paths: HostPaths, onRecovered?: ConfigRecoveredHandler): Promise<HostSettings>;
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { readFile, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { HostError } from "./wire/errors.js";
@@ -119,11 +119,12 @@ function invalidPort(field, value, source) {
119
119
  /**
120
120
  * 读 `<dataRoot>/host.json`。
121
121
  *
122
- * 三种情况分开:文件不在 → 空设置(这一档没说话);读不出来 / 不是 JSON 对象 → `config_invalid`
123
- * (写坏了要让人看见,不能当没有,否则用户改完没有任何反馈);`port` 在但不是可用端口 →
122
+ * 三种情况分开:文件不在 → 空设置(这一档没说话,静默);读不出来 / 不是 JSON / 不是对象 →
123
+ * 就是坏文件,恢复出厂:当空设置用(默认端口、默认语言)、写回一份合法的 `{}`、回调一次
124
+ * (D32:坏了就恢复出厂,下次启动不再遇到同一个坏文件);`port` 在但不是可用端口 →
124
125
  * `config_invalid`(裁定 R67:显式给出的非法值报错,不静默回落)。
125
126
  */
126
- export async function readHostSettings(paths) {
127
+ export async function readHostSettings(paths, onRecovered) {
127
128
  let raw;
128
129
  try {
129
130
  raw = await readFile(paths.hostConfigFile, "utf8");
@@ -132,29 +133,17 @@ export async function readHostSettings(paths) {
132
133
  // 文件不在是正常情况(绝大多数据根上就没有这份文件)
133
134
  if (isMissingFile(error))
134
135
  return {};
135
- throw new HostError("config_invalid", {
136
- messageKey: "config.fileBroken",
137
- params: { path: paths.hostConfigFile, reason: "unreadable" },
138
- detail: { path: paths.hostConfigFile, reason: "unreadable" },
139
- });
136
+ return await recoverHostSettings(paths, onRecovered);
140
137
  }
141
138
  let parsed;
142
139
  try {
143
140
  parsed = JSON.parse(raw);
144
141
  }
145
142
  catch {
146
- throw new HostError("config_invalid", {
147
- messageKey: "config.fileBroken",
148
- params: { path: paths.hostConfigFile, reason: "not_json" },
149
- detail: { path: paths.hostConfigFile, reason: "not_json" },
150
- });
143
+ return await recoverHostSettings(paths, onRecovered);
151
144
  }
152
145
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
153
- throw new HostError("config_invalid", {
154
- messageKey: "config.fileBroken",
155
- params: { path: paths.hostConfigFile, reason: "not_an_object" },
156
- detail: { path: paths.hostConfigFile, reason: "not_an_object" },
157
- });
146
+ return await recoverHostSettings(paths, onRecovered);
158
147
  }
159
148
  const record = parsed;
160
149
  if (!Object.prototype.hasOwnProperty.call(record, "port") || record.port === null)
@@ -164,6 +153,14 @@ export async function readHostSettings(paths) {
164
153
  throw invalidPort("port", port, "host.json");
165
154
  return { port };
166
155
  }
156
+ /**
157
+ * 坏掉之后恢复出厂:写回一份合法的 `{}`(=默认端口、默认语言),再回调一次。
158
+ */
159
+ async function recoverHostSettings(paths, onRecovered) {
160
+ await writeFile(paths.hostConfigFile, "{}\n", "utf8");
161
+ await onRecovered?.(paths.hostConfigFile);
162
+ return {};
163
+ }
167
164
  function isMissingFile(error) {
168
165
  return (typeof error === "object" &&
169
166
  error !== null &&
package/dist/i18n/en.d.ts CHANGED
@@ -34,6 +34,7 @@ export declare const en: {
34
34
  readonly "notice.upgradeRestarting": "Updated to {version}. Restarting now.";
35
35
  readonly "notice.logWarning": "Host warning: {summary}";
36
36
  readonly "notice.logError": "Host error: {summary}";
37
+ readonly "notice.configRecovered": "Config file {path} was broken; factory settings restored.";
37
38
  readonly "notice.handoffWritten": "Handoff document written to {path}.";
38
39
  readonly "notice.autoNamed": "Named this conversation \"{name}\".";
39
40
  readonly "notice.languageChanged": "This connection now speaks {language}.";
package/dist/i18n/en.js CHANGED
@@ -39,6 +39,7 @@ export const en = {
39
39
  "notice.upgradeRestarting": "Updated to {version}. Restarting now.",
40
40
  "notice.logWarning": "Host warning: {summary}",
41
41
  "notice.logError": "Host error: {summary}",
42
+ "notice.configRecovered": "Config file {path} was broken; factory settings restored.",
42
43
  "notice.handoffWritten": "Handoff document written to {path}.",
43
44
  "notice.autoNamed": "Named this conversation \"{name}\".",
44
45
  "notice.languageChanged": "This connection now speaks {language}.",
package/dist/i18n/zh.d.ts CHANGED
@@ -31,6 +31,7 @@ export declare const zh: {
31
31
  "notice.upgradeRestarting": string;
32
32
  "notice.logWarning": string;
33
33
  "notice.logError": string;
34
+ "notice.configRecovered": string;
34
35
  "notice.handoffWritten": string;
35
36
  "notice.autoNamed": string;
36
37
  "notice.languageChanged": string;
package/dist/i18n/zh.js CHANGED
@@ -35,6 +35,7 @@ export const zh = {
35
35
  "notice.upgradeRestarting": "已更新到 {version},现在重启。",
36
36
  "notice.logWarning": "宿主警告:{summary}",
37
37
  "notice.logError": "宿主错误:{summary}",
38
+ "notice.configRecovered": "配置文件 {path} 坏了,已恢复出厂设置。",
38
39
  "notice.handoffWritten": "交接文档已写到 {path}。",
39
40
  "notice.autoNamed": "给这段对话起名叫「{name}」。",
40
41
  "notice.languageChanged": "这条连接现在说{language}。",
@@ -332,7 +332,7 @@ class TurnSession {
332
332
  const agent = await this.createAgent({
333
333
  conversationId: this.conversationId,
334
334
  workspaceId: this.conversation.workspaceId,
335
- agentId: this.conversation.agentId ?? "",
335
+ agentId: this.conversation.agentId,
336
336
  modelId: this.conversation.modelId,
337
337
  thinkingLevel: this.conversation.thinkingLevel,
338
338
  scope: this.scope,
@@ -1089,7 +1089,7 @@ export class ChatRunner {
1089
1089
  }
1090
1090
  return prepare({
1091
1091
  conversationId: conversation.id,
1092
- agentId: conversation.agentId ?? "",
1092
+ agentId: conversation.agentId,
1093
1093
  modelId: conversation.modelId,
1094
1094
  input: params.input,
1095
1095
  });
@@ -601,7 +601,12 @@ export class ConfigService {
601
601
  // ---------------------------------------------------------------------------
602
602
  // 纯函数
603
603
  // ---------------------------------------------------------------------------
604
- /** 端点定义 → 对外形状(密钥只给掩码,F-F2) */
604
+ /**
605
+ * 端点定义 → 对外形状(密钥只给掩码,F-F2)。
606
+ *
607
+ * `apiKey` 在类型上是必填,但 `config.json` 是手改得了的:SDK 的读法是 `JSON.parse as AppConfig`,
608
+ * 不补字段。少写这一行的配置会走到 `?? ""`(界面显示「未设置密钥」,而不是整页读不出来)。
609
+ */
605
610
  function toEndpointView(endpoint) {
606
611
  const apiKey = endpoint.apiKey ?? "";
607
612
  return {
@@ -217,8 +217,8 @@ export declare class ConversationService {
217
217
  /**
218
218
  * 读配置。
219
219
  *
220
- * `config.json` 是手改得了的文件(F-G3 的同款思路):它坏了要让用户看见「配置用不了 +
221
- * 哪一步读不动」,而不是一句「宿主内部错误」——所以解析失败在这里就折成 `config_invalid`。
220
+ * `config.json` 是手改得了的文件,它坏了由 SDK 当场恢复出厂(D32),所以这里不再把
221
+ * 解析失败折成别的错——读到的要么是磁盘上那份,要么是已经写回磁盘的出厂配置。
222
222
  */
223
223
  private readConfig;
224
224
  private viewContext;
@@ -578,8 +578,8 @@ export class ConversationService {
578
578
  name: row.name,
579
579
  nameIsAuto: row.nameIsAuto,
580
580
  pinned: row.pinned,
581
- agentId: row.agentId ?? "",
582
- modelId: row.modelId ?? "",
581
+ agentId: row.agentId,
582
+ modelId: row.modelId,
583
583
  createdAt: iso(row.createdAt),
584
584
  updatedAt: iso(row.updatedAt),
585
585
  // 这个数来自 `listByConversation` 的长度:只有**主线**消息(不含子 Agent 的,那些在同一个
@@ -599,9 +599,9 @@ export class ConversationService {
599
599
  const agent = await this.loadAgent(row.agentId, context);
600
600
  const model = this.effectiveModel(row, context.config);
601
601
  const modelState = {
602
- modelId: model?.id ?? row.modelId ?? "",
602
+ modelId: model?.id ?? row.modelId,
603
603
  // 模型可能在配置里被删掉了:行上的 id 仍然有效(它是这段会话现在用的那个),显示名退回 id
604
- name: model?.name ?? row.modelId ?? "",
604
+ name: model?.name ?? row.modelId,
605
605
  vision: model?.vision === true,
606
606
  source: this.modelSource(row, agent, context.config),
607
607
  };
@@ -636,20 +636,11 @@ export class ConversationService {
636
636
  /**
637
637
  * 读配置。
638
638
  *
639
- * `config.json` 是手改得了的文件(F-G3 的同款思路):它坏了要让用户看见「配置用不了 +
640
- * 哪一步读不动」,而不是一句「宿主内部错误」——所以解析失败在这里就折成 `config_invalid`。
639
+ * `config.json` 是手改得了的文件,它坏了由 SDK 当场恢复出厂(D32),所以这里不再把
640
+ * 解析失败折成别的错——读到的要么是磁盘上那份,要么是已经写回磁盘的出厂配置。
641
641
  */
642
642
  async readConfig() {
643
- try {
644
- return await this.configManager.read();
645
- }
646
- catch (error) {
647
- throw new HostError("config_invalid", {
648
- params: { reason: error instanceof Error ? error.message : String(error) },
649
- detail: { path: this.configManager.configPath },
650
- cause: error,
651
- });
652
- }
643
+ return await this.configManager.read();
653
644
  }
654
645
  async viewContext(config, workspaces = [], agents = []) {
655
646
  return {
@@ -85,6 +85,13 @@ export declare class DiscoveryService {
85
85
  * `cwd` 为 `null`(还没打开项目)时只给后两层:项目侧那三层的路径得先有项目才算得出来。
86
86
  */
87
87
  mcpLayers(cwd: string | null): DiscoveryLayerPath[];
88
+ /**
89
+ * 5 层链里那两个可写的层(第 1 层与第 4 层);没有项目时 `workspace` 那层是 `null`。
90
+ *
91
+ * 位置、能不能写、归谁只在这里定一次:读(`mcpLayers`)与写(`writableMcpLayer`)拿的
92
+ * 是同一份定义。
93
+ */
94
+ private editableMcpLayers;
88
95
  /** Skill 的 4 层目录:`<ws>/.ai-zen/skills` → `<ws>/.agents/skills` → `<aiZenDir>/skills` → `~/.agents/skills` */
89
96
  skillLayers(cwd: string | null): DiscoveryLayerPath[];
90
97
  /** 用户工具的 2 层目录:`<ws>/.agents/tools` → `<aiZenDir>/tools` */
@@ -100,8 +107,6 @@ export declare class DiscoveryService {
100
107
  scopeAbilityPaths(cwd: string): ScopeAbilityPaths;
101
108
  /** 只取 MCP 的 5 层路径(喂给 SDK 的 `discoverMcpServers` / `McpScopePlugin`) */
102
109
  mcpPaths(cwd: string): string[];
103
- /** 按作用域找那一层的路径(写入时用) */
104
- mcpLayer(scope: McpEditableScope, cwd: string | null): DiscoveryLayerPath;
105
110
  /**
106
111
  * `config.getDiscovery`(F-G1 / F-G2 / F-F6)。
107
112
  *
@@ -141,7 +146,7 @@ export declare class DiscoveryService {
141
146
  layer: DiscoveryLayerView;
142
147
  removed: boolean;
143
148
  }>;
144
- /** 可写作用域 → 那一层;`workspace` 作用域必须能落到某个项目里 */
149
+ /** 可写作用域 → 那一层;`workspace` 作用域必须能落到某个项目里(空串也算没给) */
145
150
  private writableMcpLayer;
146
151
  private readMcpLayers;
147
152
  /** 读一个 mcp.json:不存在 / 坏了 / 能读 三种结论分开(F-F6) */
@@ -109,9 +109,30 @@ export class DiscoveryService {
109
109
  * `cwd` 为 `null`(还没打开项目)时只给后两层:项目侧那三层的路径得先有项目才算得出来。
110
110
  */
111
111
  mcpLayers(cwd) {
112
+ const editable = this.editableMcpLayers(cwd);
112
113
  return withoutNulls([
113
114
  projectLayer(cwd, 0, `.${FILE_MCP}`, "other"),
114
- cwd === null
115
+ editable.workspace,
116
+ projectLayer(cwd, 2, join(PROJECT_AGENTS_DIR, FILE_MCP), "other"),
117
+ editable.user,
118
+ {
119
+ depth: 4,
120
+ path: join(this.homeDir, HOME_AGENTS_DIR, FILE_MCP),
121
+ editable: false,
122
+ scope: null,
123
+ owner: "other",
124
+ },
125
+ ]);
126
+ }
127
+ /**
128
+ * 5 层链里那两个可写的层(第 1 层与第 4 层);没有项目时 `workspace` 那层是 `null`。
129
+ *
130
+ * 位置、能不能写、归谁只在这里定一次:读(`mcpLayers`)与写(`writableMcpLayer`)拿的
131
+ * 是同一份定义。
132
+ */
133
+ editableMcpLayers(cwd) {
134
+ return {
135
+ workspace: cwd === null
115
136
  ? null
116
137
  : {
117
138
  depth: 1,
@@ -120,22 +141,14 @@ export class DiscoveryService {
120
141
  scope: "workspace",
121
142
  owner: "workspace",
122
143
  },
123
- projectLayer(cwd, 2, join(PROJECT_AGENTS_DIR, FILE_MCP), "other"),
124
- {
144
+ user: {
125
145
  depth: 3,
126
146
  path: join(this.basePath, FILE_MCP),
127
147
  editable: true,
128
148
  scope: "user",
129
149
  owner: "user",
130
150
  },
131
- {
132
- depth: 4,
133
- path: join(this.homeDir, HOME_AGENTS_DIR, FILE_MCP),
134
- editable: false,
135
- scope: null,
136
- owner: "other",
137
- },
138
- ]);
151
+ };
139
152
  }
140
153
  /** Skill 的 4 层目录:`<ws>/.ai-zen/skills` → `<ws>/.agents/skills` → `<aiZenDir>/skills` → `~/.agents/skills` */
141
154
  skillLayers(cwd) {
@@ -203,17 +216,6 @@ export class DiscoveryService {
203
216
  mcpPaths(cwd) {
204
217
  return this.mcpLayers(cwd).map((layer) => layer.path);
205
218
  }
206
- /** 按作用域找那一层的路径(写入时用) */
207
- mcpLayer(scope, cwd) {
208
- const layer = this.mcpLayers(cwd).find((candidate) => candidate.scope === scope);
209
- if (!layer) {
210
- throw new HostError("unsupported", {
211
- params: { what: `the "${scope}" MCP scope` },
212
- detail: { scope, reason: "no_such_scope" },
213
- });
214
- }
215
- return layer;
216
- }
217
219
  // -------------------------------------------------------------------------
218
220
  // 读:一层的状态与条目
219
221
  // -------------------------------------------------------------------------
@@ -307,15 +309,18 @@ export class DiscoveryService {
307
309
  }
308
310
  return { layer: await this.readMcpLayerView(layer), removed };
309
311
  }
310
- /** 可写作用域 → 那一层;`workspace` 作用域必须能落到某个项目里 */
312
+ /** 可写作用域 → 那一层;`workspace` 作用域必须能落到某个项目里(空串也算没给) */
311
313
  writableMcpLayer(scope, cwd) {
312
- if (scope === "workspace" && (cwd === null || cwd.trim() === "")) {
314
+ const editable = this.editableMcpLayers(cwd);
315
+ if (scope === "user")
316
+ return editable.user;
317
+ if (editable.workspace === null || (cwd ?? "").trim() === "") {
313
318
  throw new HostError("invalid_params", {
314
319
  params: { field: "workspaceId" },
315
320
  detail: { field: "workspaceId", reason: "workspace_scope_needs_workspace" },
316
321
  });
317
322
  }
318
- return this.mcpLayer(scope, cwd);
323
+ return editable.workspace;
319
324
  }
320
325
  // -------------------------------------------------------------------------
321
326
  // 内部:MCP
@@ -976,6 +981,8 @@ async function listSubdirectories(dir) {
976
981
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
977
982
  }
978
983
  catch {
984
+ // 走到这里只剩两种情形:调用前 `stat` 过了、目录刚被删掉,或没有读权限 ——
985
+ // 两种都按「这一层没有条目」报(层本身的 broken 已经由 `readDirectory` 报过)
979
986
  return [];
980
987
  }
981
988
  }
@@ -985,6 +992,7 @@ async function listFiles(dir) {
985
992
  return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
986
993
  }
987
994
  catch {
995
+ // 同 `listSubdirectories`:目录在 `stat` 之后刚被删、或没有读权限,按空报
988
996
  return [];
989
997
  }
990
998
  }
@@ -290,8 +290,8 @@ export class ModelService {
290
290
  const input = params.imageModel;
291
291
  const config = await this.readConfig();
292
292
  const id = requireText(input?.id, "imageModel.id");
293
- requireText(input?.name, "imageModel.name");
294
- requireText(input?.modelName, "imageModel.modelName");
293
+ const name = requireText(input?.name, "imageModel.name");
294
+ const modelName = requireText(input?.modelName, "imageModel.modelName");
295
295
  const endpointId = requireText(input?.endpointId, "imageModel.endpointId");
296
296
  if (!findEndpoint(config, endpointId)) {
297
297
  throw new HostError("invalid_params", {
@@ -309,9 +309,9 @@ export class ModelService {
309
309
  defaultQuality: input.defaultQuality,
310
310
  }),
311
311
  id,
312
- name: requireText(input.name, "imageModel.name"),
312
+ name,
313
313
  endpointId,
314
- modelName: requireText(input.modelName, "imageModel.modelName"),
314
+ modelName,
315
315
  custom: true,
316
316
  };
317
317
  const next = {
@@ -111,6 +111,9 @@ export declare class QuestionService {
111
111
  *
112
112
  * 先落库再推事件:库里那一行是「多端看同一个」的真相(D22),推送只是提醒在线的那几个
113
113
  * 窗口去拉(L5 断线即作废)。
114
+ *
115
+ * 入参不做形状校验:`kind` 与两个 `messageKey` 的类型本身就是闭集(`QuestionKind` /
116
+ * `MessageKey`),而这里只有 `askHandoff` 一个调用方,候选项是常量字面量。
114
117
  */
115
118
  ask(input: AskQuestionInput, language?: Language): Promise<QuestionView>;
116
119
  /**
@@ -18,7 +18,6 @@
18
18
  import { randomUUID } from "node:crypto";
19
19
  import { DEFAULT_LANGUAGE, hasMessage, render } from "../i18n/index.js";
20
20
  import { HostError } from "../wire/errors.js";
21
- import { QUESTION_KINDS } from "../wire/states.js";
22
21
  import { readUsage } from "./MessageService.js";
23
22
  /** 交接问题的三个候选值(F-D2:自交接 / 独立摘要 / 不交接) */
24
23
  export const HANDOFF_OPTION_SELF = "self";
@@ -154,42 +153,11 @@ export class QuestionService {
154
153
  *
155
154
  * 先落库再推事件:库里那一行是「多端看同一个」的真相(D22),推送只是提醒在线的那几个
156
155
  * 窗口去拉(L5 断线即作废)。
156
+ *
157
+ * 入参不做形状校验:`kind` 与两个 `messageKey` 的类型本身就是闭集(`QuestionKind` /
158
+ * `MessageKey`),而这里只有 `askHandoff` 一个调用方,候选项是常量字面量。
157
159
  */
158
160
  async ask(input, language = DEFAULT_LANGUAGE) {
159
- if (!QUESTION_KINDS.includes(input.kind)) {
160
- throw new HostError("invalid_params", {
161
- params: { field: "kind" },
162
- detail: { field: "kind", value: input.kind, kinds: QUESTION_KINDS },
163
- });
164
- }
165
- if (!hasMessage(input.prompt.messageKey)) {
166
- throw new HostError("invalid_params", {
167
- params: { field: "prompt" },
168
- detail: { field: "prompt", reason: "unknown_message_key" },
169
- });
170
- }
171
- if (input.options.length === 0) {
172
- throw new HostError("invalid_params", {
173
- params: { field: "options" },
174
- detail: { field: "options", reason: "empty" },
175
- });
176
- }
177
- const seen = new Set();
178
- for (const option of input.options) {
179
- if (option.value.trim().length === 0 || seen.has(option.value)) {
180
- throw new HostError("invalid_params", {
181
- params: { field: "options" },
182
- detail: { field: "options", reason: "duplicate_or_empty_value", value: option.value },
183
- });
184
- }
185
- seen.add(option.value);
186
- if (!hasMessage(option.label.messageKey)) {
187
- throw new HostError("invalid_params", {
188
- params: { field: "options" },
189
- detail: { field: "options", reason: "unknown_message_key", value: option.value },
190
- });
191
- }
192
- }
193
161
  const payload = {
194
162
  code: input.code ?? input.prompt.messageKey,
195
163
  prompt: { messageKey: input.prompt.messageKey, params: input.prompt.params ?? {} },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-zen/agents-host",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "AI-Zen Agents Host — session/workspace service shared by CLI and Desktop",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -40,7 +40,7 @@
40
40
  "license": "MIT",
41
41
  "dependencies": {
42
42
  "@ai-zen/agents-core": "^4.6.0-alpha.0",
43
- "@ai-zen/agents-sdk": "^1.2.0-alpha.0",
43
+ "@ai-zen/agents-sdk": "^1.2.0-alpha.1",
44
44
  "ws": "^8.22.2"
45
45
  },
46
46
  "devDependencies": {