@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 +1 -1
- package/dist/build-info.json +4 -4
- package/dist/decision-expert-service.d.ts +45 -0
- package/dist/decision-expert-service.js +105 -0
- package/dist/decision-types.d.ts +10 -2
- package/dist/decision-types.js +6 -5
- package/dist/local-model-setup.js +7 -4
- package/dist/local-model-types.d.ts +2 -0
- package/dist/pi-session-settings.d.ts +5 -0
- package/dist/server-decision-routes.d.ts +1 -1
- package/dist/server-employee-routes.js +6 -0
- package/dist/server-pi-recommendation-routes.d.ts +7 -1
- package/dist/server-pi-recommendation-routes.js +4 -2
- package/dist/server-session-routes.js +5 -2
- package/dist/server.js +12 -8
- package/dist/team-dispatch.d.ts +1 -0
- package/dist/team-dispatch.js +2 -0
- package/dist/web-ui/content/ai-teams.js +12 -12
- package/dist/web-ui/content/scripts.js +190 -527
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
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
|
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-10-
|
|
4
|
-
"version": "4.
|
|
5
|
-
"channel": "
|
|
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
|
+
}
|
package/dist/decision-types.d.ts
CHANGED
|
@@ -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
|
|
42
|
+
export declare function parseDecisionResult(raw: unknown, request: DecisionRequest, identity?: {
|
|
43
|
+
model: string;
|
|
44
|
+
runtime: DecisionResult["runtime"];
|
|
45
|
+
}): DecisionResult;
|
package/dist/decision-types.js
CHANGED
|
@@ -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
|
|
131
|
-
|| raw.usage.
|
|
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:
|
|
134
|
-
usage: raw.usage, experimental: true, runtime:
|
|
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 ?? (() =>
|
|
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 ?
|
|
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",
|
|
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"
|
|
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
|
-
|
|
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),
|
|
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 =
|
|
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 =
|
|
1148
|
+
decisionRuntime = status.enabled ? { url: `${protocol}://${localHost}:${port}`,
|
|
1145
1149
|
...(activeSslCertPath ? { caPath: activeSslCertPath } : {}),
|
|
1146
|
-
evaluate: (value, caller, signal) =>
|
|
1147
|
-
autoAssignEvaluate =
|
|
1148
|
-
? (value, caller, signal) =>
|
|
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 = [];
|
package/dist/team-dispatch.d.ts
CHANGED
package/dist/team-dispatch.js
CHANGED
|
@@ -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)
|