@co0ontty/wand 4.83.1 → 4.84.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/README.md +4 -0
- package/dist/agent-dispatch.js +5 -1
- package/dist/ai-team-availability.d.ts +1 -1
- package/dist/ai-team-prompts.js +13 -4
- package/dist/ai-team-runner.d.ts +80 -5
- package/dist/ai-team-runner.js +332 -55
- package/dist/ai-team-types.d.ts +92 -5
- package/dist/ai-team-types.js +9 -2
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +1 -0
- package/dist/cli-api.js +3 -0
- package/dist/cli.js +18 -1
- package/dist/commit-task-archive.d.ts +3 -1
- package/dist/commit-task-archive.js +21 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +73 -0
- package/dist/conversation-mentions.d.ts +16 -0
- package/dist/conversation-mentions.js +32 -0
- package/dist/conversation-service.d.ts +90 -0
- package/dist/conversation-service.js +759 -0
- package/dist/conversation-session-preview.d.ts +9 -0
- package/dist/conversation-session-preview.js +47 -0
- package/dist/conversation-task-preview.d.ts +5 -0
- package/dist/conversation-task-preview.js +8 -0
- package/dist/conversation-types.d.ts +80 -0
- package/dist/conversation-types.js +7 -0
- package/dist/core-extension-host.d.ts +27 -0
- package/dist/core-extension-host.js +138 -0
- package/dist/core-runner.d.ts +86 -0
- package/dist/core-runner.js +757 -0
- package/dist/core-status-cli.d.ts +9 -0
- package/dist/core-status-cli.js +280 -0
- package/dist/core-turn-tracker.d.ts +12 -0
- package/dist/core-turn-tracker.js +36 -0
- package/dist/decision-runner.d.ts +6 -0
- package/dist/decision-runner.js +3 -1
- package/dist/decision-tool.d.ts +2 -0
- package/dist/decision-tool.js +49 -22
- package/dist/env-utils.d.ts +7 -3
- package/dist/env-utils.js +30 -5
- package/dist/git-quick-commit.d.ts +5 -0
- package/dist/git-quick-commit.js +9 -3
- package/dist/git-utils.js +2 -1
- package/dist/harness-engine.d.ts +110 -0
- package/dist/harness-engine.js +331 -0
- package/dist/mission-diff.js +2 -1
- package/dist/model-auto-assign.d.ts +50 -0
- package/dist/model-auto-assign.js +163 -0
- package/dist/model-group-runner.d.ts +4 -0
- package/dist/model-group-runner.js +72 -0
- package/dist/model-groups.d.ts +38 -0
- package/dist/model-groups.js +138 -0
- package/dist/models.d.ts +12 -0
- package/dist/models.js +102 -8
- package/dist/npm-update-utils.js +2 -1
- package/dist/openrouter-free-cli.d.ts +25 -0
- package/dist/openrouter-free-cli.js +109 -0
- package/dist/openrouter-free-models.d.ts +85 -0
- package/dist/openrouter-free-models.js +458 -0
- package/dist/openrouter-free-selection.d.ts +5 -0
- package/dist/openrouter-free-selection.js +7 -0
- package/dist/path-repair.d.ts +10 -0
- package/dist/path-repair.js +50 -5
- package/dist/pi-auto-resources.d.ts +12 -0
- package/dist/pi-auto-resources.js +151 -0
- package/dist/pi-execution-types.d.ts +48 -0
- package/dist/pi-execution-types.js +1 -0
- package/dist/pi-execution.d.ts +9 -0
- package/dist/pi-execution.js +187 -0
- package/dist/pi-extension-resources.d.ts +13 -0
- package/dist/pi-extension-resources.js +71 -0
- package/dist/pi-openrouter-free-bridge.d.ts +2 -0
- package/dist/pi-openrouter-free-bridge.js +80 -0
- package/dist/pi-resource-bridge.d.ts +2 -0
- package/dist/pi-resource-bridge.js +73 -0
- package/dist/pi-resource-catalog.d.ts +18 -0
- package/dist/pi-resource-catalog.js +82 -0
- package/dist/pi-resource-recommendation.d.ts +29 -0
- package/dist/pi-resource-recommendation.js +169 -0
- package/dist/pi-resource-run.d.ts +8 -0
- package/dist/pi-resource-run.js +28 -0
- package/dist/pi-session-settings.d.ts +157 -0
- package/dist/pi-session-settings.js +166 -0
- package/dist/platform-action-catalog.d.ts +24 -0
- package/dist/platform-action-catalog.js +141 -0
- package/dist/platform-action-service.d.ts +37 -0
- package/dist/platform-action-service.js +138 -0
- package/dist/platform-action-types.d.ts +125 -0
- package/dist/platform-action-types.js +7 -0
- package/dist/process-manager.d.ts +21 -1
- package/dist/process-manager.js +168 -15
- package/dist/provider-catalog.js +1 -1
- package/dist/provider-cli-updater.js +29 -8
- package/dist/provider-history-scanner.d.ts +9 -0
- package/dist/provider-history-scanner.js +107 -1
- package/dist/retention.d.ts +19 -7
- package/dist/retention.js +171 -28
- package/dist/server-ai-team-routes.js +6 -2
- package/dist/server-conversation-routes.d.ts +5 -0
- package/dist/server-conversation-routes.js +91 -0
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +85 -11
- package/dist/server-pi-recommendation-routes.d.ts +13 -0
- package/dist/server-pi-recommendation-routes.js +67 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.d.ts +4 -3
- package/dist/server-session-routes.js +182 -37
- package/dist/server-settings-routes.d.ts +5 -0
- package/dist/server-settings-routes.js +46 -0
- package/dist/server-task-routes.js +49 -4
- package/dist/server-team-dispatch-routes.d.ts +19 -0
- package/dist/server-team-dispatch-routes.js +71 -0
- package/dist/server-update-routes.d.ts +1 -1
- package/dist/server-update-routes.js +10 -5
- package/dist/server-workspace-routes.js +44 -24
- package/dist/server.d.ts +1 -0
- package/dist/server.js +148 -30
- package/dist/session-ai-context.d.ts +5 -1
- package/dist/session-ai-context.js +9 -4
- package/dist/session-registry.d.ts +4 -0
- package/dist/session-registry.js +47 -13
- package/dist/session-transport.js +8 -1
- package/dist/settings-web-access.d.ts +15 -0
- package/dist/settings-web-access.js +37 -0
- package/dist/silicon-employee-defaults.d.ts +29 -0
- package/dist/silicon-employee-defaults.js +41 -0
- package/dist/silicon-employee-dispatch.d.ts +5 -1
- package/dist/silicon-employee-dispatch.js +10 -4
- package/dist/silicon-employee-draft.d.ts +21 -4
- package/dist/silicon-employee-draft.js +103 -16
- package/dist/storage.d.ts +73 -2
- package/dist/storage.js +362 -26
- package/dist/structured-claude-protocol.js +1 -1
- package/dist/structured-client-protocol.js +11 -33
- package/dist/structured-content.d.ts +23 -0
- package/dist/structured-content.js +72 -0
- package/dist/structured-failure.d.ts +16 -0
- package/dist/structured-failure.js +36 -0
- package/dist/structured-pi-adapter.d.ts +8 -3
- package/dist/structured-pi-adapter.js +131 -30
- package/dist/structured-provider-common.d.ts +6 -0
- package/dist/structured-provider-common.js +23 -1
- package/dist/structured-runner.d.ts +45 -0
- package/dist/structured-session-manager.d.ts +78 -4
- package/dist/structured-session-manager.js +513 -129
- package/dist/structured-thinking.d.ts +15 -0
- package/dist/structured-thinking.js +46 -0
- package/dist/subagent-dispatch.d.ts +56 -0
- package/dist/subagent-dispatch.js +102 -0
- package/dist/system-employee.d.ts +4 -3
- package/dist/system-employee.js +7 -4
- package/dist/task-retention.d.ts +23 -0
- package/dist/task-retention.js +69 -0
- package/dist/task-types.d.ts +4 -0
- package/dist/task-types.js +3 -0
- package/dist/team-dispatch.d.ts +101 -0
- package/dist/team-dispatch.js +269 -0
- package/dist/tool-preview.js +7 -1
- package/dist/tui/commands.js +50 -1
- package/dist/tui/ipc-protocol.d.ts +1 -1
- package/dist/tui/ipc-server.d.ts +4 -0
- package/dist/tui/ipc-server.js +23 -0
- package/dist/turn-heartbeat.d.ts +35 -0
- package/dist/turn-heartbeat.js +64 -0
- package/dist/types.d.ts +147 -1
- package/dist/user-profile.d.ts +33 -0
- package/dist/user-profile.js +70 -0
- package/dist/web-ui/chat-message-meta.d.ts +11 -0
- package/dist/web-ui/chat-message-meta.js +47 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +795 -95
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -6808
- package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/index.d.ts +1 -1
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/markdown.d.ts +2 -0
- package/dist/web-ui/markdown.js +111 -0
- package/dist/web-ui/page.d.ts +4 -0
- package/dist/web-ui/page.js +8 -0
- package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
- package/dist/web-ui/react/local-preview/controller.js +159 -0
- package/dist/web-ui/running-activity.d.ts +63 -0
- package/dist/web-ui/running-activity.js +134 -0
- package/dist/web-ui/styles.js +2 -11
- package/package.json +7 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getDefaultModelForProvider } from "./config.js";
|
|
2
|
+
import { findModelGroup, resolveModelGroupModels } from "./model-groups.js";
|
|
3
|
+
import { OPENROUTER_FREE_SELECTOR } from "./openrouter-free-selection.js";
|
|
4
|
+
import { classifyStructuredFailure, hasStructuredExecutionProgress } from "./structured-failure.js";
|
|
5
|
+
/** Resolve at execution time; persist the selection, normalize attempt facts even without a group. */
|
|
6
|
+
export function withModelGroups(runner, config) {
|
|
7
|
+
return { start(context, observer) {
|
|
8
|
+
const provider = context.session.provider ?? context.session.structuredState?.provider ?? "claude";
|
|
9
|
+
const selected = context.session.selectedModel?.trim();
|
|
10
|
+
const selector = selected && selected !== "default" ? selected : getDefaultModelForProvider(config, provider);
|
|
11
|
+
const groupOptions = { preferDefault: !selected || selected === "default" };
|
|
12
|
+
const models = resolveModelGroupModels(config.modelGroups, provider, selector, groupOptions);
|
|
13
|
+
const group = findModelGroup(config.modelGroups, provider, selector, groupOptions);
|
|
14
|
+
const grouped = !!group && models[0] !== OPENROUTER_FREE_SELECTOR;
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
let current;
|
|
17
|
+
const start = (model) => {
|
|
18
|
+
let active = true;
|
|
19
|
+
let output = false;
|
|
20
|
+
let progress = false;
|
|
21
|
+
const isActive = () => active && !cancelled && observer.isActive();
|
|
22
|
+
const execution = runner.start({ ...context, ...(grouped ? { modelGroupModels: models } : {}),
|
|
23
|
+
session: { ...context.session, selectedModel: model || null },
|
|
24
|
+
}, { ...observer, isActive,
|
|
25
|
+
onStdout(text) { if (!isActive())
|
|
26
|
+
return; if (text.trim())
|
|
27
|
+
output = true; observer.onStdout?.(text); },
|
|
28
|
+
onEvent(event) {
|
|
29
|
+
if (!isActive())
|
|
30
|
+
return;
|
|
31
|
+
if (event.type !== "wand_pi_resource_selection")
|
|
32
|
+
output = true;
|
|
33
|
+
observer.onEvent?.(event);
|
|
34
|
+
},
|
|
35
|
+
onUpdate(state) {
|
|
36
|
+
if (!isActive())
|
|
37
|
+
return;
|
|
38
|
+
progress ||= hasStructuredExecutionProgress(state);
|
|
39
|
+
output ||= !!state.sessionId || progress;
|
|
40
|
+
observer.onUpdate({ ...state, model: state.model ?? model });
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return { ...execution, get args() { return execution.args; }, get pid() { return execution.pid; },
|
|
44
|
+
completion: execution.completion.then((result) => {
|
|
45
|
+
const failed = result.spawnError ? { ...result, exitCode: result.exitCode ?? 1,
|
|
46
|
+
primaryError: result.primaryError || result.spawnError.message } : result;
|
|
47
|
+
const failure = classifyStructuredFailure(failed, { output, progress });
|
|
48
|
+
return { ...failed, failure: failure && cancelled ? { ...failure, kind: "cancelled", retryable: false } : failure,
|
|
49
|
+
state: { ...failed.state, model: failed.state.model ?? model } };
|
|
50
|
+
}).finally(() => { active = false; }) };
|
|
51
|
+
};
|
|
52
|
+
// Synchronous registration is essential for queue/recovery ownership.
|
|
53
|
+
current = start(models[0] ?? "");
|
|
54
|
+
const first = current;
|
|
55
|
+
const completion = (async () => {
|
|
56
|
+
for (let index = 0;; index++) {
|
|
57
|
+
const result = await current.completion;
|
|
58
|
+
if (!grouped || !result.failure?.retryable || cancelled || !observer.isActive()
|
|
59
|
+
|| index + 1 >= models.length)
|
|
60
|
+
return result;
|
|
61
|
+
current = start(models[index + 1]);
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
return { ...first, get args() { return first.args; }, get pid() { return first.pid; }, completion, interrupt() {
|
|
65
|
+
cancelled = true;
|
|
66
|
+
try {
|
|
67
|
+
current.interrupt();
|
|
68
|
+
}
|
|
69
|
+
catch { /* best effort, never replay after cancellation */ }
|
|
70
|
+
} };
|
|
71
|
+
} };
|
|
72
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type SessionProvider } from "./provider-catalog.js";
|
|
2
|
+
/** Stable identities, not names, are saved in sessions/employees/default model preferences. */
|
|
3
|
+
export interface ModelGroup {
|
|
4
|
+
id: string;
|
|
5
|
+
provider: SessionProvider;
|
|
6
|
+
name: string;
|
|
7
|
+
/** First is preferred; later entries are pre-execution fallbacks, never replay after acceptance. */
|
|
8
|
+
models: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare const MODEL_GROUP_PREFIX = "wand-model-group/";
|
|
11
|
+
/**
|
|
12
|
+
* 「智能分配」:不是一个分组,而是一条只能在真实提示词到手后结算的选择。
|
|
13
|
+
* 结构化会话的第一轮、终端会话带首条输入创建时才解析成本次真正使用的分组;
|
|
14
|
+
* 没有提示词的同步场景(构建命令行、一次性文本调用)退化成默认分组。
|
|
15
|
+
*/
|
|
16
|
+
export declare const AUTO_ASSIGN_SELECTOR = "wand-auto-assign/auto";
|
|
17
|
+
export declare const AUTO_ASSIGN_LABEL = "\u667A\u80FD\u5206\u914D";
|
|
18
|
+
export declare const FREE_MODEL_GROUP_ID = "openrouter-free";
|
|
19
|
+
export declare const MODEL_GROUP_MAX_PER_PROVIDER = 32;
|
|
20
|
+
export declare const MODEL_GROUP_MAX_MEMBERS = 32;
|
|
21
|
+
export declare const DEFAULT_MODEL_GROUP_NAMES: readonly ["默认分组", "默认", "default"];
|
|
22
|
+
export declare function isModelGroupSelector(value: string | null | undefined): boolean;
|
|
23
|
+
export declare function isAutoAssignSelector(value: string | null | undefined): boolean;
|
|
24
|
+
export declare function modelGroupSelector(group: Pick<ModelGroup, "id" | "provider">): string;
|
|
25
|
+
export declare function normalizeModelGroups(value: unknown): ModelGroup[];
|
|
26
|
+
/** Names are exact and provider-scoped; stable selectors keep working after a rename. */
|
|
27
|
+
export declare function findModelGroup(groups: readonly ModelGroup[] | undefined, provider: SessionProvider, selector?: string | null, options?: {
|
|
28
|
+
preferDefault?: boolean;
|
|
29
|
+
}): ModelGroup | undefined;
|
|
30
|
+
/** A configured group wins; do not silently replace a deleted configured group. */
|
|
31
|
+
export declare function defaultModelGroupSelector(groups: readonly ModelGroup[] | undefined, provider: SessionProvider, configured?: string | null): string | undefined;
|
|
32
|
+
/** Native default is one executable candidate, not an empty candidate chain. */
|
|
33
|
+
export declare function resolveModelGroupModels(groups: readonly ModelGroup[] | undefined, provider: SessionProvider, selector: string | null | undefined, options?: {
|
|
34
|
+
preferDefault?: boolean;
|
|
35
|
+
}): string[];
|
|
36
|
+
export declare function freeModelOrder(groups: readonly ModelGroup[] | undefined): string[];
|
|
37
|
+
/** Used for free pools: saved priority first; newly verified models append, vanished models are skipped. */
|
|
38
|
+
export declare function orderModelIds(ids: readonly string[], preferred: readonly string[]): string[];
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { isSessionProvider, SESSION_PROVIDERS } from "./provider-catalog.js";
|
|
2
|
+
import { OPENROUTER_FREE_GROUP, OPENROUTER_FREE_PROVIDER, OPENROUTER_FREE_SELECTOR, isOpenRouterFreeSelector } from "./openrouter-free-selection.js";
|
|
3
|
+
export const MODEL_GROUP_PREFIX = "wand-model-group/";
|
|
4
|
+
/**
|
|
5
|
+
* 「智能分配」:不是一个分组,而是一条只能在真实提示词到手后结算的选择。
|
|
6
|
+
* 结构化会话的第一轮、终端会话带首条输入创建时才解析成本次真正使用的分组;
|
|
7
|
+
* 没有提示词的同步场景(构建命令行、一次性文本调用)退化成默认分组。
|
|
8
|
+
*/
|
|
9
|
+
export const AUTO_ASSIGN_SELECTOR = "wand-auto-assign/auto";
|
|
10
|
+
export const AUTO_ASSIGN_LABEL = "智能分配";
|
|
11
|
+
export const FREE_MODEL_GROUP_ID = "openrouter-free";
|
|
12
|
+
export const MODEL_GROUP_MAX_PER_PROVIDER = 32;
|
|
13
|
+
export const MODEL_GROUP_MAX_MEMBERS = 32;
|
|
14
|
+
export const DEFAULT_MODEL_GROUP_NAMES = ["默认分组", "默认", "default"];
|
|
15
|
+
const GROUP_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
16
|
+
export function isModelGroupSelector(value) {
|
|
17
|
+
return value?.startsWith(MODEL_GROUP_PREFIX) === true;
|
|
18
|
+
}
|
|
19
|
+
export function isAutoAssignSelector(value) {
|
|
20
|
+
return value?.trim() === AUTO_ASSIGN_SELECTOR;
|
|
21
|
+
}
|
|
22
|
+
/** 「智能分配」在只拿到选择器、拿不到提示词时的等价写法:按默认分组解析。 */
|
|
23
|
+
function modelSelectorWithoutAutoAssign(selector) {
|
|
24
|
+
const value = selector?.trim() ?? "";
|
|
25
|
+
return isAutoAssignSelector(value) ? "" : value;
|
|
26
|
+
}
|
|
27
|
+
export function modelGroupSelector(group) {
|
|
28
|
+
return group.id === FREE_MODEL_GROUP_ID && group.provider === "pi"
|
|
29
|
+
? OPENROUTER_FREE_SELECTOR : `${MODEL_GROUP_PREFIX}${group.provider}/${group.id}`;
|
|
30
|
+
}
|
|
31
|
+
export function normalizeModelGroups(value) {
|
|
32
|
+
if (!Array.isArray(value) || value.length > MODEL_GROUP_MAX_PER_PROVIDER * SESSION_PROVIDERS.length + 1) {
|
|
33
|
+
throw new Error("模型分组必须是有界数组。");
|
|
34
|
+
}
|
|
35
|
+
const identities = new Set();
|
|
36
|
+
const names = new Set();
|
|
37
|
+
const counts = new Map();
|
|
38
|
+
return value.map((entry) => {
|
|
39
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
40
|
+
throw new Error("模型分组格式无效。");
|
|
41
|
+
const row = entry;
|
|
42
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
43
|
+
const name = typeof row.name === "string" ? row.name.trim() : "";
|
|
44
|
+
if (!GROUP_ID.test(id) || !isSessionProvider(row.provider))
|
|
45
|
+
throw new Error("模型分组标识或工具无效。");
|
|
46
|
+
const provider = row.provider;
|
|
47
|
+
if (!name || name.length > 40 || /[\x00-\x1f\x7f]/.test(name))
|
|
48
|
+
throw new Error("分组名称需为 1–40 个字符。");
|
|
49
|
+
const free = id === FREE_MODEL_GROUP_ID;
|
|
50
|
+
if (free && (provider !== "pi" || name !== OPENROUTER_FREE_GROUP))
|
|
51
|
+
throw new Error("免费分组的工具与名称不可修改。");
|
|
52
|
+
if (!free && provider === "pi" && name === OPENROUTER_FREE_GROUP)
|
|
53
|
+
throw new Error("免费分组是内置分组,请直接调整它的顺序。");
|
|
54
|
+
const identity = `${provider}/${id}`;
|
|
55
|
+
const named = `${provider}/${name}`;
|
|
56
|
+
if (identities.has(identity) || names.has(named))
|
|
57
|
+
throw new Error("同一工具的分组标识或名称不能重复。");
|
|
58
|
+
identities.add(identity);
|
|
59
|
+
names.add(named);
|
|
60
|
+
const count = (counts.get(provider) ?? 0) + (free ? 0 : 1);
|
|
61
|
+
if (count > MODEL_GROUP_MAX_PER_PROVIDER)
|
|
62
|
+
throw new Error(`每个工具最多 ${MODEL_GROUP_MAX_PER_PROVIDER} 个分组。`);
|
|
63
|
+
counts.set(provider, count);
|
|
64
|
+
if (!Array.isArray(row.models) || (!free && !row.models.length)
|
|
65
|
+
|| row.models.length > (free ? 128 : MODEL_GROUP_MAX_MEMBERS)) {
|
|
66
|
+
throw new Error(`分组需包含 1–${MODEL_GROUP_MAX_MEMBERS} 个模型;免费分组可留空使用自动顺序。`);
|
|
67
|
+
}
|
|
68
|
+
const models = row.models.map((model) => {
|
|
69
|
+
const trimmed = typeof model === "string" ? model.trim() : "";
|
|
70
|
+
if (!trimmed || trimmed === "default" || trimmed.length > 256 || /[\s\x00-\x1f\x7f]/.test(trimmed)
|
|
71
|
+
|| isModelGroupSelector(trimmed) || isAutoAssignSelector(trimmed)
|
|
72
|
+
|| trimmed === OPENROUTER_FREE_SELECTOR || trimmed.startsWith("-")) {
|
|
73
|
+
throw new Error("组内必须是具体模型 ID,不能嵌套分组或使用默认值。");
|
|
74
|
+
}
|
|
75
|
+
if (isOpenRouterFreeSelector(trimmed) && provider !== "pi")
|
|
76
|
+
throw new Error("免费模型仅属于 one 的 Agent。");
|
|
77
|
+
if (free && !trimmed.startsWith(`${OPENROUTER_FREE_PROVIDER}/`))
|
|
78
|
+
throw new Error("免费分组只能包含免费池中的模型。");
|
|
79
|
+
return trimmed;
|
|
80
|
+
});
|
|
81
|
+
if (new Set(models).size !== models.length)
|
|
82
|
+
throw new Error("同一分组中的模型不能重复。");
|
|
83
|
+
return { id, provider, name, models };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Names are exact and provider-scoped; stable selectors keep working after a rename. */
|
|
87
|
+
export function findModelGroup(groups, provider, selector, options = {}) {
|
|
88
|
+
const candidates = (groups ?? []).filter((entry) => entry.provider === provider);
|
|
89
|
+
const value = modelSelectorWithoutAutoAssign(selector);
|
|
90
|
+
if (isModelGroupSelector(value) || value === OPENROUTER_FREE_SELECTOR) {
|
|
91
|
+
return candidates.find((entry) => modelGroupSelector(entry) === value);
|
|
92
|
+
}
|
|
93
|
+
if (value && value !== "default") {
|
|
94
|
+
const named = candidates.find((entry) => entry.name === value);
|
|
95
|
+
if (named || !options.preferDefault)
|
|
96
|
+
return named;
|
|
97
|
+
}
|
|
98
|
+
const named = DEFAULT_MODEL_GROUP_NAMES.map((name) => candidates.find((entry) => entry.name === name))
|
|
99
|
+
.find((entry) => entry !== undefined);
|
|
100
|
+
return named ?? candidates.find((entry) => entry.id !== FREE_MODEL_GROUP_ID) ?? candidates[0];
|
|
101
|
+
}
|
|
102
|
+
/** A configured group wins; do not silently replace a deleted configured group. */
|
|
103
|
+
export function defaultModelGroupSelector(groups, provider, configured) {
|
|
104
|
+
const value = configured?.trim();
|
|
105
|
+
// 智能分配是明确选择,必须原样带到会话上,由第一轮提示词结算。
|
|
106
|
+
if (isModelGroupSelector(value) || value === OPENROUTER_FREE_SELECTOR || isAutoAssignSelector(value))
|
|
107
|
+
return value;
|
|
108
|
+
const group = findModelGroup(groups, provider, value, { preferDefault: true });
|
|
109
|
+
return group ? modelGroupSelector(group) : undefined;
|
|
110
|
+
}
|
|
111
|
+
/** Native default is one executable candidate, not an empty candidate chain. */
|
|
112
|
+
export function resolveModelGroupModels(groups, provider, selector, options = {}) {
|
|
113
|
+
const value = modelSelectorWithoutAutoAssign(selector);
|
|
114
|
+
if (value === OPENROUTER_FREE_SELECTOR) {
|
|
115
|
+
if (provider !== "pi")
|
|
116
|
+
throw new Error("免费分组仅属于 one 的 Agent。");
|
|
117
|
+
return [value];
|
|
118
|
+
}
|
|
119
|
+
const group = findModelGroup(groups, provider, value, options);
|
|
120
|
+
if (group?.id === FREE_MODEL_GROUP_ID)
|
|
121
|
+
return [OPENROUTER_FREE_SELECTOR];
|
|
122
|
+
if (group) {
|
|
123
|
+
if (!group.models.length)
|
|
124
|
+
throw new Error("所选模型分组已删除、为空或不属于当前工具,请重新选择。");
|
|
125
|
+
return [...group.models];
|
|
126
|
+
}
|
|
127
|
+
if (isModelGroupSelector(value))
|
|
128
|
+
throw new Error("所选模型分组已删除、为空或不属于当前工具,请重新选择。");
|
|
129
|
+
return [value === "default" ? "" : value];
|
|
130
|
+
}
|
|
131
|
+
export function freeModelOrder(groups) {
|
|
132
|
+
return groups?.find((entry) => entry.id === FREE_MODEL_GROUP_ID && entry.provider === "pi")?.models ?? [];
|
|
133
|
+
}
|
|
134
|
+
/** Used for free pools: saved priority first; newly verified models append, vanished models are skipped. */
|
|
135
|
+
export function orderModelIds(ids, preferred) {
|
|
136
|
+
const available = new Set(ids);
|
|
137
|
+
return [...new Set([...preferred.filter((id) => available.has(id)), ...ids])];
|
|
138
|
+
}
|
package/dist/models.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type PiModelEndpointDiscoveryOptions } from "./pi-model-discovery.js";
|
|
2
2
|
import { ClaudeModelInfo } from "./types.js";
|
|
3
|
+
import { type ModelGroup } from "./model-groups.js";
|
|
3
4
|
/**
|
|
4
5
|
* The complete server-side model catalog. Keep this separate from the Claude
|
|
5
6
|
* verification cache: the former is a client-facing snapshot for every
|
|
@@ -41,6 +42,9 @@ export interface ModelRefreshOptions {
|
|
|
41
42
|
piEndpointDiscovery?: PiModelEndpointDiscoveryOptions & {
|
|
42
43
|
enabled: boolean;
|
|
43
44
|
};
|
|
45
|
+
managedPiModels?: () => readonly ClaudeModelInfo[];
|
|
46
|
+
managedPiModelMembers?: () => ClaudeModelInfo[];
|
|
47
|
+
modelGroups?: () => readonly ModelGroup[];
|
|
44
48
|
now?: () => Date;
|
|
45
49
|
}
|
|
46
50
|
export interface ThinkingEffortLevel {
|
|
@@ -79,6 +83,9 @@ export declare const FALLBACK_PROVIDER_THINKING: ProviderThinkingEfforts;
|
|
|
79
83
|
*/
|
|
80
84
|
export declare function withConfiguredDefaultModelLabels<T extends object>(catalog: T, defaults: Readonly<Record<string, string>>): T;
|
|
81
85
|
export interface ModelCatalogSnapshot extends ModelCache {
|
|
86
|
+
modelGroups?: ModelGroup[];
|
|
87
|
+
/** Verified concrete free members, used only when editing the pool order. */
|
|
88
|
+
freeModels?: ClaudeModelInfo[];
|
|
82
89
|
/** SHA-256 of the catalog excluding `refreshedAt`. Changes only with content. */
|
|
83
90
|
revision: string;
|
|
84
91
|
}
|
|
@@ -150,12 +157,17 @@ export declare class ModelCatalogService {
|
|
|
150
157
|
private readonly getOptions;
|
|
151
158
|
private cache;
|
|
152
159
|
private revision;
|
|
160
|
+
private publishedRevision;
|
|
153
161
|
private hasPersistedSnapshot;
|
|
154
162
|
private refreshPromise;
|
|
155
163
|
private inFlightIncludesVerification;
|
|
156
164
|
private readonly changeListeners;
|
|
157
165
|
constructor(getOptions: () => ModelRefreshOptions);
|
|
166
|
+
private withManagedPiModels;
|
|
167
|
+
publishManagedPiModels(): void;
|
|
158
168
|
snapshot(): ModelCatalogSnapshot;
|
|
169
|
+
/** Preference changes publish immediately without re-running CLI discovery or altering stored history. */
|
|
170
|
+
publishModelGroups(): void;
|
|
159
171
|
/** 目录内容真正变化时通知。客户端靠它刷新下拉,不用自己再跑一遍 CLI。 */
|
|
160
172
|
onChanged(listener: (result: ModelCatalogRefreshResult) => void): () => void;
|
|
161
173
|
refresh(request?: ModelCatalogRefreshRequest): Promise<ModelCatalogRefreshResult>;
|
package/dist/models.js
CHANGED
|
@@ -8,6 +8,8 @@ import Anthropic from "@anthropic-ai/sdk";
|
|
|
8
8
|
import { resolveChildEnv } from "./env-utils.js";
|
|
9
9
|
import { discoverPiEndpointModels } from "./pi-model-discovery.js";
|
|
10
10
|
import { extractSemver } from "./version-utils.js";
|
|
11
|
+
import { AUTO_ASSIGN_LABEL, AUTO_ASSIGN_SELECTOR, FREE_MODEL_GROUP_ID, isAutoAssignSelector, isModelGroupSelector, modelGroupSelector, orderModelIds } from "./model-groups.js";
|
|
12
|
+
import { OPENROUTER_FREE_GROUP } from "./openrouter-free-selection.js";
|
|
11
13
|
const execFileAsync = promisify(execFile);
|
|
12
14
|
const CLAUDE_VERIFICATION_CACHE_KEY = "claude-model-verifications-v1";
|
|
13
15
|
/**
|
|
@@ -59,7 +61,7 @@ const QODER_FALLBACK_MODELS = [
|
|
|
59
61
|
{ id: "ultimate", label: "Ultimate" },
|
|
60
62
|
];
|
|
61
63
|
const PI_FALLBACK_MODELS = [
|
|
62
|
-
{ id: "default", label: "跟随
|
|
64
|
+
{ id: "default", label: "跟随 one 的 Agent 默认", alias: true },
|
|
63
65
|
];
|
|
64
66
|
/**
|
|
65
67
|
* Gemini CLI 没有列模型的子命令,只能给出稳定别名(`packages/core/src/config/models.ts`
|
|
@@ -124,10 +126,14 @@ export function withConfiguredDefaultModelLabels(catalog, defaults) {
|
|
|
124
126
|
if (at < 0)
|
|
125
127
|
continue;
|
|
126
128
|
const entry = list[at];
|
|
127
|
-
|
|
129
|
+
const selected = list.find((item) => item?.id === configured);
|
|
130
|
+
const name = configured.startsWith("wand-model-group/") || configured === "wand-openrouter-free/auto"
|
|
131
|
+
? selected?.label || "模型分组"
|
|
132
|
+
: isAutoAssignSelector(configured) ? selected?.label || AUTO_ASSIGN_LABEL : configured;
|
|
133
|
+
if (String(entry.label ?? "").includes(name))
|
|
128
134
|
continue;
|
|
129
135
|
const updated = [...list];
|
|
130
|
-
updated[at] = { ...entry, label: `跟随服务端默认(${
|
|
136
|
+
updated[at] = { ...entry, label: `跟随服务端默认(${name})` };
|
|
131
137
|
next ??= { ...catalog };
|
|
132
138
|
next[field] = updated;
|
|
133
139
|
}
|
|
@@ -168,7 +174,7 @@ function normalizeClaudeModelId(value) {
|
|
|
168
174
|
if (typeof value !== "string")
|
|
169
175
|
return null;
|
|
170
176
|
const id = value.trim();
|
|
171
|
-
return MODEL_ID_PATTERN.test(id) ? id : null;
|
|
177
|
+
return MODEL_ID_PATTERN.test(id) && !isModelGroupSelector(id) ? id : null;
|
|
172
178
|
}
|
|
173
179
|
function formatClaudeModelLabel(id, displayName) {
|
|
174
180
|
const name = displayName?.trim();
|
|
@@ -389,6 +395,9 @@ async function probeQoderModels(runner, env) {
|
|
|
389
395
|
}
|
|
390
396
|
}
|
|
391
397
|
async function probePiModels(runner, env, options) {
|
|
398
|
+
const agentDir = options.piEndpointDiscovery?.agentDir;
|
|
399
|
+
if (agentDir)
|
|
400
|
+
env = { ...env, PI_CODING_AGENT_DIR: agentDir };
|
|
392
401
|
const rpcText = await commandText(runner, "pi", ["--mode", "rpc", "--no-session"], env, 12_000, `${JSON.stringify({ id: "wand-models", type: "get_available_models" })}\n`);
|
|
393
402
|
const fromRpc = parsePiRpcModels(rpcText);
|
|
394
403
|
let configuredModels = fromRpc.length > 1 || (fromRpc.length === 1 && fromRpc[0]?.id !== "default")
|
|
@@ -790,7 +799,7 @@ export function parsePiRpcModels(stdout) {
|
|
|
790
799
|
return [
|
|
791
800
|
{
|
|
792
801
|
id: "default",
|
|
793
|
-
label: "跟随
|
|
802
|
+
label: "跟随 one 的 Agent 默认",
|
|
794
803
|
alias: true,
|
|
795
804
|
...(union.length ? { reasoningEfforts: union } : {}),
|
|
796
805
|
},
|
|
@@ -823,7 +832,7 @@ export function parsePiModels(stdout) {
|
|
|
823
832
|
if (!discovered.length)
|
|
824
833
|
return cloneModels(PI_FALLBACK_MODELS);
|
|
825
834
|
return [
|
|
826
|
-
{ id: "default", label: "跟随
|
|
835
|
+
{ id: "default", label: "跟随 one 的 Agent 默认", alias: true },
|
|
827
836
|
...discovered,
|
|
828
837
|
];
|
|
829
838
|
}
|
|
@@ -953,6 +962,7 @@ function parsePersistedModelInfo(value) {
|
|
|
953
962
|
id,
|
|
954
963
|
label,
|
|
955
964
|
...(typeof value.alias === "boolean" ? { alias: value.alias } : {}),
|
|
965
|
+
...(typeof value.group === "string" ? { group: value.group.slice(0, 80) } : {}),
|
|
956
966
|
...(source ? { source } : {}),
|
|
957
967
|
...(availability ? { availability } : {}),
|
|
958
968
|
...(note ? { note } : {}),
|
|
@@ -1150,6 +1160,7 @@ export class ModelCatalogService {
|
|
|
1150
1160
|
getOptions;
|
|
1151
1161
|
cache;
|
|
1152
1162
|
revision;
|
|
1163
|
+
publishedRevision;
|
|
1153
1164
|
hasPersistedSnapshot;
|
|
1154
1165
|
refreshPromise = null;
|
|
1155
1166
|
inFlightIncludesVerification = false;
|
|
@@ -1159,11 +1170,92 @@ export class ModelCatalogService {
|
|
|
1159
1170
|
const initialOptions = getOptions();
|
|
1160
1171
|
const persisted = loadPersistedModelCatalog(initialOptions.storage);
|
|
1161
1172
|
this.cache = persisted ? cloneCache(persisted.catalog) : createInitialCache(initialOptions);
|
|
1162
|
-
this.
|
|
1173
|
+
this.cache.piModels = this.withManagedPiModels(this.cache.piModels);
|
|
1174
|
+
this.revision = catalogRevision(this.cache);
|
|
1163
1175
|
this.hasPersistedSnapshot = persisted !== null;
|
|
1176
|
+
this.publishedRevision = this.snapshot().revision;
|
|
1177
|
+
}
|
|
1178
|
+
withManagedPiModels(models) {
|
|
1179
|
+
const managed = this.getOptions().managedPiModels;
|
|
1180
|
+
if (!managed)
|
|
1181
|
+
return models;
|
|
1182
|
+
const entries = managed();
|
|
1183
|
+
const ids = new Set(entries.map((model) => model.id));
|
|
1184
|
+
return [...models.filter((model) => !model.id.startsWith("wand-openrouter-free/") && !ids.has(model.id)), ...entries];
|
|
1185
|
+
}
|
|
1186
|
+
publishManagedPiModels() {
|
|
1187
|
+
const next = { ...this.cache, piModels: this.withManagedPiModels(this.cache.piModels) };
|
|
1188
|
+
const revision = catalogRevision(next);
|
|
1189
|
+
const projectionChanged = this.snapshot().revision !== this.publishedRevision;
|
|
1190
|
+
if (revision === this.revision && !projectionChanged)
|
|
1191
|
+
return;
|
|
1192
|
+
next.refreshedAt = new Date().toISOString();
|
|
1193
|
+
this.cache = next;
|
|
1194
|
+
this.revision = revision;
|
|
1195
|
+
savePersistedModelCatalog(this.getOptions().storage, this.cache, revision);
|
|
1196
|
+
const result = { ...this.snapshot(), changed: true, checkedAt: next.refreshedAt };
|
|
1197
|
+
this.publishedRevision = result.revision;
|
|
1198
|
+
for (const listener of this.changeListeners) {
|
|
1199
|
+
try {
|
|
1200
|
+
listener(result);
|
|
1201
|
+
}
|
|
1202
|
+
catch { /* notification must not prevent publication */ }
|
|
1203
|
+
}
|
|
1164
1204
|
}
|
|
1165
1205
|
snapshot() {
|
|
1166
|
-
|
|
1206
|
+
const options = this.getOptions();
|
|
1207
|
+
const configured = options.modelGroups?.() ?? [];
|
|
1208
|
+
const catalog = cloneCache(this.cache);
|
|
1209
|
+
// Virtual group identities are never concrete models or CLI verification targets.
|
|
1210
|
+
for (const field of Object.values(PROVIDER_MODEL_LIST_FIELDS)) {
|
|
1211
|
+
catalog[field] = catalog[field].filter((model) => !isModelGroupSelector(model.id));
|
|
1212
|
+
}
|
|
1213
|
+
const freeModels = options.managedPiModelMembers?.() ?? [];
|
|
1214
|
+
const groups = structuredClone(configured.filter((group) => group.id !== FREE_MODEL_GROUP_ID));
|
|
1215
|
+
if (options.managedPiModelMembers) {
|
|
1216
|
+
const saved = configured.find((group) => group.id === FREE_MODEL_GROUP_ID && group.provider === "pi");
|
|
1217
|
+
groups.push({ id: FREE_MODEL_GROUP_ID, provider: "pi", name: OPENROUTER_FREE_GROUP,
|
|
1218
|
+
models: orderModelIds(freeModels.map((model) => model.id), saved?.models ?? []) });
|
|
1219
|
+
}
|
|
1220
|
+
for (const group of groups) {
|
|
1221
|
+
if (group.id === FREE_MODEL_GROUP_ID)
|
|
1222
|
+
continue; // Already published by OpenRouter as a single pool option.
|
|
1223
|
+
const list = catalog[PROVIDER_MODEL_LIST_FIELDS[group.provider]];
|
|
1224
|
+
const members = group.provider === "pi" ? [...list, ...freeModels] : list;
|
|
1225
|
+
const memberInfo = group.models.map((id) => members.find((model) => model.id === id));
|
|
1226
|
+
const efforts = memberInfo.every((model) => model?.reasoningEfforts?.length)
|
|
1227
|
+
? memberInfo[0].reasoningEfforts.filter((level) => memberInfo.every((model) => model.reasoningEfforts.some((item) => item.effort === level.effort))) : [];
|
|
1228
|
+
list.push({ id: modelGroupSelector(group), label: group.name, group: "模型分组",
|
|
1229
|
+
...(efforts.length ? { reasoningEfforts: structuredClone(efforts) } : {}) });
|
|
1230
|
+
}
|
|
1231
|
+
const projection = { ...catalog, ...(groups.length ? { modelGroups: groups } : {}),
|
|
1232
|
+
...(options.managedPiModelMembers ? { freeModels } : {}) };
|
|
1233
|
+
for (const field of Object.values(PROVIDER_MODEL_LIST_FIELDS)) {
|
|
1234
|
+
const list = projection[field];
|
|
1235
|
+
if (!list.some((model) => model.id === AUTO_ASSIGN_SELECTOR)) {
|
|
1236
|
+
const entry = { id: AUTO_ASSIGN_SELECTOR, label: AUTO_ASSIGN_LABEL, alias: false,
|
|
1237
|
+
source: "builtin", availability: "default",
|
|
1238
|
+
note: "按第一条提示词在本工具的模型分组里自动选一个" };
|
|
1239
|
+
// 默认项排在最前;智能分配紧跟其后,其余具体模型与分组保持原顺序。
|
|
1240
|
+
if (list[0]?.id === "default")
|
|
1241
|
+
list.splice(1, 0, entry);
|
|
1242
|
+
else
|
|
1243
|
+
list.unshift(entry);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return { ...projection, revision: groups.length
|
|
1247
|
+
? createHash("sha256").update(`${this.revision}:${JSON.stringify(groups)}:${JSON.stringify(freeModels)}`).digest("hex") : this.revision };
|
|
1248
|
+
}
|
|
1249
|
+
/** Preference changes publish immediately without re-running CLI discovery or altering stored history. */
|
|
1250
|
+
publishModelGroups() {
|
|
1251
|
+
const result = { ...this.snapshot(), changed: true, checkedAt: new Date().toISOString() };
|
|
1252
|
+
this.publishedRevision = result.revision;
|
|
1253
|
+
for (const listener of this.changeListeners) {
|
|
1254
|
+
try {
|
|
1255
|
+
listener(result);
|
|
1256
|
+
}
|
|
1257
|
+
catch { /* notification must not prevent saving */ }
|
|
1258
|
+
}
|
|
1167
1259
|
}
|
|
1168
1260
|
/** 目录内容真正变化时通知。客户端靠它刷新下拉,不用自己再跑一遍 CLI。 */
|
|
1169
1261
|
onChanged(listener) {
|
|
@@ -1197,6 +1289,7 @@ export class ModelCatalogService {
|
|
|
1197
1289
|
};
|
|
1198
1290
|
const checkedAt = (options.now?.() ?? new Date()).toISOString();
|
|
1199
1291
|
const discovered = await discoverModelCache(options, this.cache);
|
|
1292
|
+
discovered.piModels = this.withManagedPiModels(discovered.piModels);
|
|
1200
1293
|
const discoveredRevision = catalogRevision(discovered);
|
|
1201
1294
|
const changed = !this.hasPersistedSnapshot || discoveredRevision !== this.revision;
|
|
1202
1295
|
if (changed) {
|
|
@@ -1208,6 +1301,7 @@ export class ModelCatalogService {
|
|
|
1208
1301
|
this.hasPersistedSnapshot = Boolean(options.storage);
|
|
1209
1302
|
}
|
|
1210
1303
|
const result = { ...this.snapshot(), changed, checkedAt };
|
|
1304
|
+
this.publishedRevision = result.revision;
|
|
1211
1305
|
if (changed) {
|
|
1212
1306
|
for (const listener of this.changeListeners) {
|
|
1213
1307
|
try {
|
package/dist/npm-update-utils.js
CHANGED
|
@@ -17,6 +17,7 @@ import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readF
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { promisify } from "node:util";
|
|
20
|
+
import { buildChildEnv } from "./env-utils.js";
|
|
20
21
|
import { whichSync } from "./path-repair.js";
|
|
21
22
|
import { getErrorMessage } from "./error-utils.js";
|
|
22
23
|
import { compareWandInstallOrder, extractSemver } from "./version-utils.js";
|
|
@@ -114,7 +115,7 @@ function getChildEnv() {
|
|
|
114
115
|
pathEntries.push(entry);
|
|
115
116
|
}
|
|
116
117
|
return {
|
|
117
|
-
...
|
|
118
|
+
...buildChildEnv(true),
|
|
118
119
|
PATH: pathEntries.join(path.delimiter),
|
|
119
120
|
};
|
|
120
121
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Model, Api } from "@earendil-works/pi-ai";
|
|
2
|
+
import { OpenRouterFreeModelsService, type FreeModelRequirements } from "./openrouter-free-models.js";
|
|
3
|
+
import type { WandStorage } from "./storage.js";
|
|
4
|
+
import type { PreparedPiResources } from "./pi-resource-run.js";
|
|
5
|
+
import type { StructuredRunnerContext } from "./structured-runner.js";
|
|
6
|
+
export interface OpenRouterFreeCliPolicy {
|
|
7
|
+
databasePath: string;
|
|
8
|
+
model: Model<Api>;
|
|
9
|
+
}
|
|
10
|
+
export interface OpenRouterFreeCliDependencies {
|
|
11
|
+
storage: WandStorage;
|
|
12
|
+
service: OpenRouterFreeModelsService;
|
|
13
|
+
}
|
|
14
|
+
/** Only an opaque, revocable capability goes into the child env; never the connector Key. */
|
|
15
|
+
export declare function prepareOpenRouterFreeCli(deps: OpenRouterFreeCliDependencies, context: StructuredRunnerContext): PreparedPiResources;
|
|
16
|
+
/** Read-only projection: no migrations or shared catalog writes from a running Pi child. */
|
|
17
|
+
export declare function openOpenRouterFreeCliAccess(databasePath: string, token: string, fetchImpl?: typeof fetch): {
|
|
18
|
+
selector: string;
|
|
19
|
+
requirements: FreeModelRequirements;
|
|
20
|
+
service: OpenRouterFreeModelsService;
|
|
21
|
+
assertCurrent: () => void;
|
|
22
|
+
close: () => void;
|
|
23
|
+
};
|
|
24
|
+
/** The CLI, not its original Server owner, revokes scope on shutdown. */
|
|
25
|
+
export declare function revokeOpenRouterFreeCliAccess(databasePath: string, token: string): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { DatabaseSync } from "node:sqlite";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { decryptVaultSecret } from "./password-manager.js";
|
|
8
|
+
import { OpenRouterFreeModelsService, OPENROUTER_FREE_SELECTOR } from "./openrouter-free-models.js";
|
|
9
|
+
/** Only an opaque, revocable capability goes into the child env; never the connector Key. */
|
|
10
|
+
export function prepareOpenRouterFreeCli(deps, context) {
|
|
11
|
+
const selector = context.session.selectedModel;
|
|
12
|
+
const effort = context.session.thinkingEffort?.split(":").at(-1);
|
|
13
|
+
const requirements = { preferReasoning: !!effort && effort !== "off",
|
|
14
|
+
...(context.modelGroupModels ? { allowedSelectors: context.modelGroupModels } : {}) };
|
|
15
|
+
const resolved = deps.service.resolve(selector, requirements) ?? deps.service.resolve(OPENROUTER_FREE_SELECTOR, requirements);
|
|
16
|
+
if (!resolved)
|
|
17
|
+
throw new Error("免费模型已下架或 OpenRouter Key 未配置,请同步免费分组并重新选择模型。");
|
|
18
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "wand-pi-free-"));
|
|
19
|
+
let token;
|
|
20
|
+
const close = () => {
|
|
21
|
+
if (token) {
|
|
22
|
+
try {
|
|
23
|
+
deps.storage.revokeOpenRouterFreeAccess(token);
|
|
24
|
+
}
|
|
25
|
+
catch { /* The durable CLI owns cleanup after a web restart. */ }
|
|
26
|
+
}
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
token = deps.storage.issueOpenRouterFreeAccess(context.session.id, selector, createHash("sha256").update(resolved.apiKey).digest("hex"), requirements);
|
|
31
|
+
const file = path.join(dir, "model.json");
|
|
32
|
+
const policy = { databasePath: deps.storage.databasePath(), model: resolved.model };
|
|
33
|
+
writeFileSync(file, JSON.stringify(policy), { mode: 0o600 });
|
|
34
|
+
const js = fileURLToPath(new URL("./pi-openrouter-free-bridge.js", import.meta.url));
|
|
35
|
+
return { args: ["--extension", existsSync(js) ? js : js.replace(/\.js$/, ".ts")],
|
|
36
|
+
env: { ...context.env, WAND_PI_FREE_POLICY: file, WAND_PI_FREE_TOKEN: token }, close };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
close();
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Read-only projection: no migrations or shared catalog writes from a running Pi child. */
|
|
44
|
+
export function openOpenRouterFreeCliAccess(databasePath, token, fetchImpl = globalThis.fetch) {
|
|
45
|
+
if (!path.isAbsolute(databasePath) || !/^wf_[A-Za-z0-9_-]{43}$/.test(token)) {
|
|
46
|
+
throw new Error("免费模型调用授权无效。");
|
|
47
|
+
}
|
|
48
|
+
const db = new DatabaseSync(databasePath, { readOnly: true });
|
|
49
|
+
const hash = createHash("sha256").update(token).digest("hex");
|
|
50
|
+
const scope = () => {
|
|
51
|
+
const row = db.prepare(`SELECT a.* FROM openrouter_free_access a JOIN command_sessions s ON s.id = a.session_id
|
|
52
|
+
WHERE a.token_hash = ? AND a.expires_at > ? AND s.provider = 'pi' AND s.session_kind = 'structured'
|
|
53
|
+
AND s.status = 'running'`).get(hash, Date.now());
|
|
54
|
+
if (!row)
|
|
55
|
+
throw new Error("免费模型调用授权已失效,请重新发送。");
|
|
56
|
+
return row;
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const access = scope();
|
|
60
|
+
const readConfig = (key) => {
|
|
61
|
+
const value = db.prepare("SELECT value FROM app_config WHERE key = ?").get(key)?.value;
|
|
62
|
+
return typeof value === "string" ? value : null;
|
|
63
|
+
};
|
|
64
|
+
const localCache = new Map();
|
|
65
|
+
const storage = {
|
|
66
|
+
getConnectorToken: () => {
|
|
67
|
+
const current = scope();
|
|
68
|
+
const encrypted = db.prepare("SELECT token FROM connectors WHERE provider = 'openrouter'").get()?.token;
|
|
69
|
+
const key = typeof encrypted === "string" ? decryptVaultSecret(encrypted, readConfig("appSecret")) : null;
|
|
70
|
+
if (!key || createHash("sha256").update(key).digest("hex") !== current.credential_hash) {
|
|
71
|
+
throw new Error("OpenRouter Key 已变化,本次免费调用已停止,请重新发送。");
|
|
72
|
+
}
|
|
73
|
+
return key;
|
|
74
|
+
},
|
|
75
|
+
getConfigValue: (key) => localCache.get(key) ?? readConfig(key),
|
|
76
|
+
setConfigValue: (key, value) => { scope(); localCache.set(key, value); },
|
|
77
|
+
getPreference: (key, fallback) => {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(readConfig(key) ?? "null") ?? fallback;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
saveConnector: () => { throw new Error("CLI 不可修改 OpenRouter Key。"); },
|
|
86
|
+
deleteConnector: () => { throw new Error("CLI 不可删除 OpenRouter Key。"); },
|
|
87
|
+
};
|
|
88
|
+
const service = new OpenRouterFreeModelsService(storage, fetchImpl);
|
|
89
|
+
return { selector: String(access.selector), requirements: JSON.parse(String(access.requirements_json)),
|
|
90
|
+
service, assertCurrent: () => { storage.getConnectorToken("openrouter"); },
|
|
91
|
+
close: () => { service.dispose(); db.close(); } };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
db.close();
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** The CLI, not its original Server owner, revokes scope on shutdown. */
|
|
99
|
+
export function revokeOpenRouterFreeCliAccess(databasePath, token) {
|
|
100
|
+
const db = new DatabaseSync(databasePath);
|
|
101
|
+
try {
|
|
102
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
103
|
+
db.prepare("DELETE FROM openrouter_free_access WHERE token_hash = ?")
|
|
104
|
+
.run(createHash("sha256").update(token).digest("hex"));
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
db.close();
|
|
108
|
+
}
|
|
109
|
+
}
|