@co0ontty/wand 4.87.0 → 4.88.0-beta.gb1ee942

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 CHANGED
@@ -271,7 +271,7 @@ wand config:set port 9443
271
271
 
272
272
  ### 语音识别
273
273
 
274
- Web、Android、iOS 可在语音输入设置中选择「服务端识别」或「客户端本地识别」。服务端使用按需安装的 whisper.cpp 多语言模型,支持无 GPU CPU 服务器、Mac mini Metal 与可选 CUDA;模型不随普通构建下载。部署、模型大小、鉴权和隐私边界见 [服务端语音识别](docs/server-speech.md)。
274
+ Web、Android、iOS 可在语音输入设置中选择「服务端识别」或「客户端本地识别」。服务端使用按需安装的 whisper.cpp 多语言模型,支持无 GPU CPU 服务器、Mac mini Metal 与可选 CUDA;模型不随普通构建下载。部署、模型大小、鉴权和隐私边界见 [服务端语音识别](docs/server-speech.md)。设置内下载/初始化 LAYA 与语音模型,以及「决策专家」的本机性能检查和默认本地/免费调用链,见 [本地模型管理](docs/local-model-setup.md)。
275
275
 
276
276
  ### 可选本地决策(实验性)
277
277
 
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "d2f0f8fd339f2c9b668c596b443fb8d1b0756783",
3
- "builtAt": "2026-10-09T02:40:15.225Z",
4
- "version": "4.87.0",
5
- "channel": "stable"
2
+ "commit": "b1ee94297085a10acc1bd9b7da943e1a12435851",
3
+ "builtAt": "2026-10-09T07:09:46.631Z",
4
+ "version": "4.88.0-beta.gb1ee942",
5
+ "channel": "beta"
6
6
  }
@@ -0,0 +1,45 @@
1
+ import { type DecisionHardwareAssessment } from "./decision-hardware.js";
2
+ import { type DecisionRequest, type DecisionResult } from "./decision-types.js";
3
+ import type { DecisionService } from "./decision-service.js";
4
+ import type { SiliconEmployee } from "./ai-team-types.js";
5
+ import { type OpenRouterFreeModelsService } from "./openrouter-free-models.js";
6
+ import type { WandConfig } from "./types.js";
7
+ import type { WandTaskAgent } from "./task-types.js";
8
+ interface DecisionExpertDeps {
9
+ employee(): SiliconEmployee | null;
10
+ local: Pick<DecisionService, "status" | "evaluate">;
11
+ free: Pick<OpenRouterFreeModelsService, "status" | "resolveForCall">;
12
+ config: Pick<WandConfig, "modelGroups">;
13
+ hardware?: () => DecisionHardwareAssessment;
14
+ timeoutMs?: number;
15
+ /** Tests may inject a free, no-tool structured generator; production never silently swaps engine. */
16
+ generate?: (agent: WandTaskAgent, request: DecisionRequest, employee: SiliconEmployee, signal?: AbortSignal) => Promise<{
17
+ answers: unknown;
18
+ inputTokens: number;
19
+ outputTokens: number;
20
+ model: string;
21
+ }>;
22
+ }
23
+ /** Dedicated bounded employee evaluator. LAYA is not sent to a chat provider as a fake model ID. */
24
+ export declare class DecisionExpertService {
25
+ private readonly deps;
26
+ private active;
27
+ constructor(deps: DecisionExpertDeps);
28
+ status(): {
29
+ enabled: boolean;
30
+ supported: boolean;
31
+ configured: boolean;
32
+ state: string;
33
+ queued: number;
34
+ completed: number;
35
+ failed: number;
36
+ experimental: true;
37
+ hardware: DecisionHardwareAssessment;
38
+ employeeId: string;
39
+ employeeName: string;
40
+ notice: string | null;
41
+ };
42
+ evaluate(value: unknown, caller: string, signal?: AbortSignal): Promise<DecisionResult>;
43
+ private generate;
44
+ }
45
+ export {};
@@ -0,0 +1,105 @@
1
+ import { DECISION_EXPERT_ID, DECISION_EXPERT_KEY, WAND_LOCAL_DECISION_MODEL } from "./decision-expert-identity.js";
2
+ import { decisionHardware } from "./decision-hardware.js";
3
+ import { DecisionError, parseDecisionRequest, parseDecisionResult } from "./decision-types.js";
4
+ import { isOpenRouterFreeSelector, OPENROUTER_FREE_ROUTING } from "./openrouter-free-models.js";
5
+ import { freeModelOrder } from "./model-groups.js";
6
+ import { normalizeContext } from "@earendil-works/pi-ai";
7
+ /** Dedicated bounded employee evaluator. LAYA is not sent to a chat provider as a fake model ID. */
8
+ export class DecisionExpertService {
9
+ deps;
10
+ active = 0;
11
+ constructor(deps) {
12
+ this.deps = deps;
13
+ }
14
+ status() {
15
+ const local = this.deps.local.status(), employee = this.deps.employee(), free = this.deps.free.status();
16
+ const hardware = (this.deps.hardware ?? decisionHardware)();
17
+ const backup = employee?.agents.some(agent => agent.provider === "pi" && agent.engine === "sdk" && agent.kind === "structured" && isOpenRouterFreeSelector(agent.model)) && free.configured && free.modelCount > 0;
18
+ const primary = employee?.agents.some(agent => agent.provider === "pi" && agent.engine === "sdk" && agent.kind === "structured" && agent.model === WAND_LOCAL_DECISION_MODEL);
19
+ return { enabled: !!employee, supported: true, configured: !!employee && ((!!primary && hardware.suitable && local.supported && local.enabled && local.configured) || !!backup),
20
+ state: local.state, queued: this.active, completed: local.completed, failed: local.failed, experimental: true,
21
+ hardware, employeeId: employee?.id ?? DECISION_EXPERT_ID, employeeName: employee?.name ?? "决策专家",
22
+ notice: !hardware.suitable ? hardware.message : !local.enabled || !local.configured ? "本地决策未就绪,可初始化 LAYA 或配置「决策专家」员工调用链使用备用免费分组。" : null };
23
+ }
24
+ async evaluate(value, caller, signal) {
25
+ const request = parseDecisionRequest(value), employee = this.deps.employee();
26
+ if (!employee || employee.systemKey !== DECISION_EXPERT_KEY)
27
+ throw new DecisionError("UNAVAILABLE", "请先配置「决策专家」员工调用链。", 503);
28
+ if (signal?.aborted)
29
+ throw new DecisionError("CANCELLED", "决策已取消。", 499);
30
+ if (this.active >= 8)
31
+ throw new DecisionError("BUSY", "决策专家队列已满,请稍后重试。", 429);
32
+ this.active += 1;
33
+ const deadline = new AbortController();
34
+ const timeout = new DecisionError("TIMEOUT", "决策专家请求超时,请稍后重试或调整该员工调用链。", 504);
35
+ const timer = setTimeout(() => deadline.abort(timeout), this.deps.timeoutMs ?? 45_000);
36
+ signal = signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal;
37
+ try {
38
+ const hardware = (this.deps.hardware ?? decisionHardware)();
39
+ for (let candidate = 0; candidate < employee.agents.length; candidate += 1) {
40
+ const agent = employee.agents[candidate];
41
+ if (agent.model === WAND_LOCAL_DECISION_MODEL) {
42
+ if (agent.provider !== "pi" || agent.engine !== "sdk" || agent.kind !== "structured")
43
+ throw new DecisionError("CONFIGURATION_REQUIRED", "本地 LAYA 候选需使用 Wand 决策执行入口;请核对「决策专家」调用链。", 503);
44
+ const local = this.deps.local.status();
45
+ if (!hardware.suitable || !local.enabled || !local.configured || !local.supported)
46
+ continue;
47
+ try {
48
+ const result = await this.deps.local.evaluate(request, caller, signal);
49
+ return { ...result, executor: { employeeId: employee.id, candidate, source: "local" } };
50
+ }
51
+ catch (error) {
52
+ if (deadline.signal.aborted)
53
+ throw timeout;
54
+ if (signal?.aborted || (error instanceof DecisionError && ["CANCELLED", "INVALID_REQUEST", "RATE_LIMIT", "BUSY"].includes(error.code)))
55
+ throw error;
56
+ // This is read-only inference, never a replay of a tool/task side effect.
57
+ continue;
58
+ }
59
+ }
60
+ if (!isOpenRouterFreeSelector(agent.model)) {
61
+ // A missing free pool never authorizes using an arbitrary/paid provider behind the user's back.
62
+ throw new DecisionError("CONFIGURATION_REQUIRED", "此建议仅允许已配置的本地决策或 Wand 免费分组;请核对「决策专家」调用链与费用边界。", 503);
63
+ }
64
+ if (agent.provider !== "pi" || agent.engine !== "sdk")
65
+ throw new DecisionError("CONFIGURATION_REQUIRED", "建议的免费分组候选需使用 Wand Agent;不会用另一引擎冒充,请配置「决策专家」调用链。", 503);
66
+ try {
67
+ const generated = await (this.deps.generate ?? this.generate.bind(this))(agent, request, employee, signal);
68
+ const result = parseDecisionResult({ answers: generated.answers, usage: { input_tokens: generated.inputTokens, output_tokens: generated.outputTokens, truncated: false } }, request, { model: generated.model, runtime: "decision-expert" });
69
+ return { ...result, executor: { employeeId: employee.id, candidate, source: "free-group" } };
70
+ }
71
+ catch (error) {
72
+ if (deadline.signal.aborted)
73
+ throw timeout;
74
+ if (signal?.aborted)
75
+ throw new DecisionError("CANCELLED", "决策专家请求已取消。", 499);
76
+ // Once a free model request was sent, don't silently repeat it via another candidate.
77
+ throw new DecisionError("CONFIGURATION_REQUIRED", "决策专家免费分组调用失败或结果无效,请同步免费分组并配置该员工调用链。", 503);
78
+ }
79
+ }
80
+ throw new DecisionError("CONFIGURATION_REQUIRED", !hardware.suitable ? hardware.message : "决策专家没有当前可用的候选;请初始化 LAYA 或配置该员工备用调用链。", 503);
81
+ }
82
+ finally {
83
+ clearTimeout(timer);
84
+ this.active -= 1;
85
+ }
86
+ }
87
+ async generate(agent, request, employee, signal) {
88
+ const order = freeModelOrder(this.deps.config.modelGroups);
89
+ const checked = await this.deps.free.resolveForCall(agent.model, signal, order.length ? { allowedSelectors: order } : {});
90
+ const { streamSimple } = await import("@earendil-works/pi-ai/api/openai-completions");
91
+ const model = checked.model;
92
+ const message = await streamSimple(model, normalizeContext({
93
+ systemPrompt: `${employee.prompt}\n只输出JSON对象 {"answers":{...}}。对每题保留type;choice给choice和所有criteria的probabilities(总和1),score给score和以0开始索引的probabilities(总和1),noul给0到1的noul。不得输出工具调用、执行操作或增加候选。概率是实验性辅助而非授权。`,
94
+ messages: [{ role: "user", content: JSON.stringify(request), timestamp: Date.now() }], tools: [],
95
+ }), { apiKey: checked.apiKey, signal, maxTokens: Math.min(4096, model.maxTokens), toolChoice: "none",
96
+ onPayload: payload => ({ ...payload, provider: OPENROUTER_FREE_ROUTING }) }).result();
97
+ if (message.stopReason === "error" || message.stopReason === "aborted" || message.content.some(part => part.type === "toolCall"))
98
+ throw new Error("Decision expert failed");
99
+ const text = message.content.filter(part => part.type === "text").map(part => part.text).join("").trim();
100
+ if (text.length > 32768)
101
+ throw new Error("Decision expert output too large");
102
+ const body = JSON.parse(text);
103
+ return { answers: body.answers, inputTokens: message.usage.input, outputTokens: message.usage.output, model: `${model.provider}/${model.id}` };
104
+ }
105
+ }
@@ -26,7 +26,12 @@ export interface DecisionResult {
26
26
  truncated?: boolean;
27
27
  };
28
28
  experimental: true;
29
- runtime: "laya-mlx";
29
+ runtime: "laya-mlx" | "decision-expert";
30
+ executor?: {
31
+ employeeId: string;
32
+ candidate: number;
33
+ source: "local" | "free-group";
34
+ };
30
35
  }
31
36
  export declare class DecisionError extends Error {
32
37
  readonly code: string;
@@ -34,4 +39,7 @@ export declare class DecisionError extends Error {
34
39
  constructor(code: string, message: string, status?: number);
35
40
  }
36
41
  export declare function parseDecisionRequest(value: unknown): DecisionRequest;
37
- export declare function parseDecisionResult(raw: unknown, request: DecisionRequest): DecisionResult;
42
+ export declare function parseDecisionResult(raw: unknown, request: DecisionRequest, identity?: {
43
+ model: string;
44
+ runtime: DecisionResult["runtime"];
45
+ }): DecisionResult;
@@ -93,7 +93,7 @@ export function parseDecisionRequest(value) {
93
93
  // Detach the accepted input from caller mutations while it waits in the queue.
94
94
  return JSON.parse(JSON.stringify({ state: value.state, questions }));
95
95
  }
96
- export function parseDecisionResult(raw, request) {
96
+ export function parseDecisionResult(raw, request, identity = { model: DECISION_CHECKPOINT, runtime: "laya-mlx" }) {
97
97
  const fail = () => { throw new DecisionError("INVALID_RESULT", "决策引擎返回了无效结果。", 502); };
98
98
  if (!record(raw) || !record(raw.answers) || !record(raw.usage))
99
99
  return fail();
@@ -127,9 +127,10 @@ export function parseDecisionResult(raw, request) {
127
127
  }
128
128
  }
129
129
  if (!Number.isSafeInteger(raw.usage.input_tokens) || raw.usage.input_tokens < 0
130
- || raw.usage.input_tokens > DECISION_MAX_QUESTIONS * 1024 || raw.usage.output_tokens !== 0
131
- || raw.usage.truncated !== false)
130
+ || (identity.runtime === "laya-mlx" && raw.usage.input_tokens > DECISION_MAX_QUESTIONS * 1024)
131
+ || !Number.isSafeInteger(raw.usage.output_tokens) || raw.usage.output_tokens < 0
132
+ || (identity.runtime === "laya-mlx" && raw.usage.output_tokens !== 0) || raw.usage.truncated !== false)
132
133
  return fail();
133
- return { model: DECISION_CHECKPOINT, answers: answers,
134
- usage: raw.usage, experimental: true, runtime: "laya-mlx" };
134
+ return { model: identity.model, answers: answers,
135
+ usage: raw.usage, experimental: true, runtime: identity.runtime };
135
136
  }
@@ -8,6 +8,8 @@ import { ModelFileStore } from "./model-file-store.js";
8
8
  import { runModelSetup } from "./model-setup-process.js";
9
9
  import { speechModel } from "./speech-models.js";
10
10
  import { systemEnvValue } from "./env-utils.js";
11
+ import { decisionHardware } from "./decision-hardware.js";
12
+ import { DECISION_EXPERT_ID } from "./decision-expert-identity.js";
11
13
  /** Deployment jobs only; existing inference owners retain their protocols, rate limits and capabilities. */
12
14
  export class LocalModelSetupService {
13
15
  deps;
@@ -24,7 +26,7 @@ export class LocalModelSetupService {
24
26
  this.root = path.join(deps.configDir, "local-models", "laya");
25
27
  this.manifest = deps.layaFiles ?? LAYA_FILES;
26
28
  }
27
- supported() { return (this.deps.supportedLaya ?? (() => process.platform === "darwin" && process.arch === "arm64"))(); }
29
+ supported() { return (this.deps.supportedLaya ?? (() => decisionHardware().suitable))(); }
28
30
  managedModel() { return path.join(this.root, "model"); }
29
31
  async layaModel() {
30
32
  const current = this.deps.decisionConfig().modelPath;
@@ -50,15 +52,16 @@ export class LocalModelSetupService {
50
52
  const [model, python, speech] = await Promise.all([this.layaModel(), this.python(), this.deps.speech.status()]);
51
53
  const decision = this.deps.decisions.status();
52
54
  const supported = this.supported();
55
+ const hardware = decisionHardware();
53
56
  const operation = this.operations.get("laya") ?? null;
54
57
  const selected = speech.models.find((value) => value.id === speech.settings.model);
55
58
  return {
56
59
  laya: { kind: "laya", label: "LAYA 本地决策", supported,
57
- reason: !supported ? "LAYA-MLX 当前仅支持 Apple Silicon macOS / Metal;不是聊天或授权模型。"
60
+ reason: !supported ? hardware.message
58
61
  : !model.downloaded ? "模型尚未下载或完整性校验失败。" : !python ? "独立 Python/MLX 运行时尚未初始化。" : null,
59
62
  enabled: decision.enabled, model: LAYA_REPOSITORY, modelSize: this.manifest.reduce((sum, file) => sum + file.size, 0),
60
63
  downloaded: model.downloaded, runtimeAvailable: !!python, initialized: decision.state === "ready", busy: this.jobs.has("laya") || decision.queued > 0 || decision.state === "loading",
61
- operation: operation ? { ...operation } : null },
64
+ operation: operation ? { ...operation } : null, hardware, decisionEmployeeId: DECISION_EXPERT_ID },
62
65
  speech: { kind: "speech", label: "服务端语音识别", supported: ["darwin", "linux", "win32"].includes(process.platform), reason: speech.reason,
63
66
  enabled: speech.settings.enabled, model: speech.settings.model, modelSize: selected?.size ?? 0,
64
67
  downloaded: selected?.downloaded ?? false, runtimeAvailable: speech.runtime.available, initialized: speech.initialized === true,
@@ -86,7 +89,7 @@ export class LocalModelSetupService {
86
89
  if (this.disposed)
87
90
  throw new DecisionError("UNAVAILABLE", "模型管理已关闭。", 503);
88
91
  if (kind === "laya" && !this.supported())
89
- throw new DecisionError("UNSUPPORTED", "LAYA-MLX 需要 Apple Silicon macOS 与 Metal。", 409);
92
+ throw new DecisionError("UNSUPPORTED", `LAYA-MLX 需要 Apple Silicon macOS 与 Metal;${decisionHardware().message}`, 409);
90
93
  const input = this.parse(kind, raw);
91
94
  if (this.jobs.has(kind))
92
95
  throw new DecisionError("BUSY", "该模型已有安装任务,请等待或取消。", 409);
@@ -23,6 +23,8 @@ export interface LocalModelStatus {
23
23
  initialized: boolean;
24
24
  busy: boolean;
25
25
  operation: ModelSetupOperation | null;
26
+ hardware?: import("./decision-hardware.js").DecisionHardwareAssessment;
27
+ decisionEmployeeId?: string;
26
28
  }
27
29
  export interface LocalModelsStatus {
28
30
  laya: LocalModelStatus;
@@ -123,6 +123,11 @@ export interface PiSettingsResponse {
123
123
  recommendationReason?: string;
124
124
  autoResourcesAvailable?: boolean;
125
125
  autoResourcesReason?: string;
126
+ decisionAdvisor?: {
127
+ notice: string | null;
128
+ employeeId: string;
129
+ hardware: import("./decision-hardware.js").DecisionHardwareAssessment;
130
+ };
126
131
  /** Older automatic-resource servers do not infer CodeMode. */
127
132
  autoCodemodeAvailable?: boolean;
128
133
  /** Absent on older servers; clients must not pretend skill locks are supported. */
@@ -4,7 +4,7 @@ import type { DecisionService } from "./decision-service.js";
4
4
  /** Mounted before generic /api auth so inference-only capabilities cannot authorize any other route. */
5
5
  export declare function registerDecisionRoutes(app: Express, deps: {
6
6
  storage: WandStorage;
7
- decisions: DecisionService;
7
+ decisions: Pick<DecisionService, "status" | "evaluate">;
8
8
  requireAuth: RequestHandler;
9
9
  requireSessions: RequestHandler;
10
10
  }): void;
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, parseSiliconEmployeeTags, siliconEmployeeTags, } from "./ai-team-types.js";
3
3
  import { getErrorMessage } from "./error-utils.js";
4
+ import { DECISION_EXPERT_KEY, isLocalDecisionModel } from "./decision-expert-identity.js";
5
+ import { OPENROUTER_FREE_SELECTOR, isOpenRouterFreeSelector } from "./openrouter-free-selection.js";
4
6
  import { asyncRoute } from "./express-async.js";
5
7
  import { bodyObject, sendRouteError, text } from "./server-request.js";
6
8
  import { parseTaskAgent } from "./server-task-routes.js";
@@ -110,6 +112,10 @@ export function parseSystemEmployeeAgents(value, existing) {
110
112
  throw new Error("请选择有效的 CLI 工具或 Wand Agent。");
111
113
  if (parsed.kind !== "structured")
112
114
  throw new Error("硅基员工只支持结构化会话。");
115
+ if (existing.systemKey === DECISION_EXPERT_KEY && (parsed.provider !== "pi" || parsed.engine !== "sdk"
116
+ || !(isLocalDecisionModel(parsed.model) || parsed.model === OPENROUTER_FREE_SELECTOR || isOpenRouterFreeSelector(parsed.model)))) {
117
+ throw new Error("决策专家仅支持 LAYA 或 Wand 免费分组的决策候选;请使用 Wand 内部执行入口。");
118
+ }
113
119
  return parsed;
114
120
  });
115
121
  if (new Set(agents.map(agentKey)).size !== agents.length) {
@@ -3,7 +3,13 @@ import type { DecisionService } from "./decision-service.js";
3
3
  import type { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import type { SessionRegistry } from "./session-registry.js";
5
5
  import type { WandConfig } from "./types.js";
6
- export type PiRecommendationRuntime = Pick<DecisionService, "evaluate" | "status">;
6
+ export type PiRecommendationRuntime = Pick<DecisionService, "evaluate"> & {
7
+ status(): ReturnType<DecisionService["status"]> & Partial<{
8
+ notice: string | null;
9
+ employeeId: string;
10
+ hardware: import("./decision-hardware.js").DecisionHardwareAssessment;
11
+ }>;
12
+ };
7
13
  /** Uses the parent's login + sessions-scope middleware; inference-only tokens cannot enter this route. */
8
14
  export declare function registerPiRecommendationRoute(app: Express, deps: {
9
15
  structured: StructuredSessionManager;
@@ -26,7 +26,8 @@ export function registerPiRecommendationRoute(app, deps) {
26
26
  }
27
27
  const status = deps.decisions?.status();
28
28
  if (!deps.decisions || !status?.enabled || !status.supported || !status.configured || settings.localDecision === false) {
29
- throw new DecisionError("UNAVAILABLE", "本地决策未启用或不可用,原选择未改变。", 503);
29
+ const notice = status && "notice" in status && typeof status.notice === "string" ? status.notice : "本地决策未启用或不可用,原选择未改变;请配置「决策专家」调用链。";
30
+ throw new DecisionError("UNAVAILABLE", notice, 503);
30
31
  }
31
32
  const input = parsePiRecommendationRequest(req.body);
32
33
  if (active.has(id))
@@ -46,8 +47,9 @@ export function registerPiRecommendationRoute(app, deps) {
46
47
  }
47
48
  if (abort.signal.aborted)
48
49
  throw new DecisionError("TIMEOUT", "推荐已取消或超时,原选择未改变。", 504);
50
+ const advisor = deps.decisions.status();
49
51
  if (!res.destroyed)
50
- res.json(result);
52
+ res.json({ ...result, ...("hardware" in advisor ? { decisionAdvisor: advisor } : {}) });
51
53
  }
52
54
  catch (error) {
53
55
  if (res.destroyed)
@@ -637,7 +637,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
637
637
  const archived = snapshot.archived;
638
638
  const modifiable = !archived;
639
639
  const goalAvailable = installed.some((item) => item.goal);
640
- const localDecisionAvailable = config.localDecision?.enabled === true;
640
+ const localDecisionAvailable = config.localDecision?.enabled === true || decisions?.status().enabled === true;
641
641
  const inventory = await discoverPiResources(config, snapshot.cwd);
642
642
  const resourceCatalog = { ...inventory.catalog, supported: modifiable && !sdk && inventory.catalog.supported,
643
643
  reason: archived ? "请先恢复已归档的会话。" : sdk ? "Wand Agent 尚不支持 Skills / MCP 会话选择。" : inventory.catalog.reason };
@@ -657,10 +657,13 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
657
657
  && status?.enabled === true && status.supported && status.configured;
658
658
  const recommendationReason = !resourceCatalog.supported ? resourceCatalog.reason
659
659
  : settings.localDecision === false ? "本会话已关闭本地决策。"
660
- : !recommendationAvailable ? "本地决策未启用或运行环境不可用。" : "";
660
+ : !recommendationAvailable ? status && "notice" in status && typeof status.notice === "string" ? status.notice : "本地决策未启用或运行环境不可用;请配置「决策专家」调用链。" : "";
661
661
  return { recommendationAvailable, recommendationReason,
662
662
  autoResourcesAvailable: recommendationAvailable, autoResourcesReason: recommendationReason,
663
663
  autoCodemodeAvailable: recommendationAvailable, skillLocksAvailable: resourceCatalog.supported,
664
+ ...(status?.hardware && typeof status.employeeId === "string" ? { decisionAdvisor: {
665
+ notice: status.notice ?? null, employeeId: status.employeeId, hardware: status.hardware
666
+ } } : {}),
664
667
  settings: effectivePiSessionSettings(settings, resolution.engine), engine: resolution.engine,
665
668
  available: sdk && modifiable, toolsAvailable: !sdk && modifiable,
666
669
  reason: archived ? "请先恢复已归档的会话。" : resolution.reason,
package/dist/server.js CHANGED
@@ -31,6 +31,8 @@ import { registerSettingsRoutes } from "./server-settings-routes.js";
31
31
  import { registerSpeechRoutes } from "./server-speech-routes.js";
32
32
  import { registerLocalModelRoutes } from "./server-local-model-routes.js";
33
33
  import { LocalModelSetupService } from "./local-model-setup.js";
34
+ import { DecisionExpertService } from "./decision-expert-service.js";
35
+ import { DECISION_EXPERT_KEY } from "./decision-expert-identity.js";
34
36
  import { SpeechService } from "./speech-service.js";
35
37
  import { appTokenLoginPayload, buildStructuredChatPersonaPayload, isBrowserExtensionOrigin, resolveAppConnectCode, resolveStructuredChatAvatarPath, verifyAppToken, } from "./server-app-connect.js";
36
38
  import { firstHeaderValue, sendRouteError } from "./server-request.js";
@@ -325,6 +327,8 @@ export async function startServer(config, configPath, options = {}) {
325
327
  const authService = new AuthService(storage);
326
328
  const settingsAccess = new SettingsWebAccess();
327
329
  const decisions = new DecisionService(config.localDecision);
330
+ const decisionExpert = new DecisionExpertService({ local: decisions, free: openRouter, config,
331
+ employee: () => storage.getSystemSiliconEmployee(DECISION_EXPERT_KEY) });
328
332
  // 默认模型优先读存储(UI 改设置后实时生效),未设置时由 getPreference 回落到 config。
329
333
  const getCurrentDefaultModels = () => ({
330
334
  claude: storage.getPreference("pref:defaultModel", config.defaultModel ?? ""),
@@ -689,7 +693,7 @@ export async function startServer(config, configPath, options = {}) {
689
693
  const { hasActiveTurns, activeTurnCount } = structuredSessions.getCoreTurnStatus();
690
694
  res.set("Cache-Control", "no-store").json({ hasActiveTurns, activeTurnCount });
691
695
  });
692
- registerDecisionRoutes(app, { storage, decisions, requireAuth, requireSessions });
696
+ registerDecisionRoutes(app, { storage, decisions: decisionExpert, requireAuth, requireSessions });
693
697
  app.use("/api", requireAuth);
694
698
  app.get("/api/daemon-maintenance", (_req, res) => {
695
699
  res.set("Cache-Control", "no-store").json(daemonMaintenance.status());
@@ -829,7 +833,7 @@ export async function startServer(config, configPath, options = {}) {
829
833
  notifyTeamChanged: (teamId) => notifyAiTeamRun({ kind: "ai-team-definition", teamId }),
830
834
  });
831
835
  // 无指派派工:本地决策模型建议名单 → 确认开工(不自动派工,见 AGENTS)。
832
- registerTeamDispatchRoutes(app, { storage, runner: aiTeams, decisions });
836
+ registerTeamDispatchRoutes(app, { storage, runner: aiTeams, decisions: decisionExpert });
833
837
  registerSiliconEmployeeRoutes(app, {
834
838
  storage,
835
839
  config,
@@ -863,7 +867,7 @@ export async function startServer(config, configPath, options = {}) {
863
867
  }
864
868
  registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config, sessionRegistry, (cwd) => {
865
869
  recordRecentPath(storage, cwd);
866
- }, (event) => wsManager.emitEvent(event), decisions);
870
+ }, (event) => wsManager.emitEvent(event), decisionExpert);
867
871
  registerClaudeHistoryRoutes(app, processes, storage);
868
872
  registerWorkspaceRoutes(app, storage, sessionRegistry, { config });
869
873
  registerMissionRoutes(app, missions);
@@ -1139,13 +1143,13 @@ export async function startServer(config, configPath, options = {}) {
1139
1143
  function refreshDecisionRuntime() {
1140
1144
  const address = server.address();
1141
1145
  const port = typeof address === "object" && address ? address.port : config.port;
1142
- const status = decisions.status();
1146
+ const status = decisionExpert.status();
1143
1147
  const localHost = config.host === "::1" ? "[::1]" : config.host === "0.0.0.0" || config.host === "::" ? "127.0.0.1" : config.host;
1144
- decisionRuntime = config.localDecision?.enabled ? { url: `${protocol}://${localHost}:${port}`,
1148
+ decisionRuntime = status.enabled ? { url: `${protocol}://${localHost}:${port}`,
1145
1149
  ...(activeSslCertPath ? { caPath: activeSslCertPath } : {}),
1146
- evaluate: (value, caller, signal) => decisions.evaluate(value, caller, signal) } : null;
1147
- autoAssignEvaluate = config.localDecision?.enabled && status.supported && status.configured
1148
- ? (value, caller, signal) => decisions.evaluate(value, caller, signal) : undefined;
1150
+ evaluate: (value, caller, signal) => decisionExpert.evaluate(value, caller, signal) } : null;
1151
+ autoAssignEvaluate = status.enabled
1152
+ ? (value, caller, signal) => decisionExpert.evaluate(value, caller, signal) : undefined;
1149
1153
  }
1150
1154
  let bindAddr = config.host === "0.0.0.0" ? "0.0.0.0" : config.host;
1151
1155
  const collectedUrls = [];
@@ -97,5 +97,6 @@ export declare function assertDispatchDecisionReady(decisions: {
97
97
  enabled: boolean;
98
98
  supported: boolean;
99
99
  configured: boolean;
100
+ notice?: string | null;
100
101
  };
101
102
  }): void;
@@ -260,6 +260,8 @@ export async function startTeamDispatch(request) {
260
260
  /** 决策服务可用性:不可用时明确报错,绝不编造名单。 */
261
261
  export function assertDispatchDecisionReady(decisions) {
262
262
  const status = decisions.status();
263
+ if ((!status.enabled || !status.supported || !status.configured) && status.notice)
264
+ throw new Error(status.notice);
263
265
  if (!status.enabled)
264
266
  throw new Error("本地决策未启用(localDecision.enabled=false),无法用决策模型选人。");
265
267
  if (!status.supported)