larkin 0.4.27 → 0.4.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/agent-cli.mjs +4 -2
- package/dist/app/agent-config.mjs +45 -29
- package/dist/app/cli.mjs +16 -6
- package/dist/app/local-control.mjs +16 -3
- package/dist/app/pi-auth-cli.mjs +227 -17
- package/dist/app/runtime-model-directory.mjs +3 -2
- package/dist/app/runtime-process.mjs +42 -5
- package/dist/app/setup.mjs +15 -14
- package/dist/dashboard/dashboard-config-controller.mjs +145 -13
- package/dist/dashboard/dashboard-view-model.mjs +3 -2
- package/dist/dashboard/web/assets/dashboard.css +1 -1
- package/dist/dashboard/web/assets/dashboard.js +15 -15
- package/dist/feishu/host-shell.cjs +68 -16
- package/dist/feishu/host-shell.mjs +69 -17
- package/dist/platform/config.cjs +67 -16
- package/dist/platform/config.mjs +57 -16
- package/dist/runtime/pi-official-auth.cjs +594 -0
- package/dist/runtime/pi-official-auth.mjs +53 -0
- package/dist/runtime/pi-provider-config.cjs +26 -3
- package/dist/runtime/pi-provider-config.mjs +22 -2
- package/dist/runtime/pi-provider-login.mjs +240 -0
- package/dist/runtime/runtime-adapters.mjs +24 -4
- package/dist/runtime/runtime-host.cjs +215 -16
- package/dist/runtime/runtime-host.mjs +216 -17
- package/dist/runtime/runtime-model-catalog.cjs +4 -3
- package/dist/runtime/runtime-model-catalog.mjs +4 -3
- package/dist/runtime/runtime-readiness.cjs +123 -3
- package/dist/runtime/runtime-readiness.mjs +116 -4
- package/dist/runtime/user-runtime.cjs +51 -0
- package/dist/runtime/user-runtime.mjs +41 -0
- package/dist/setup/bot-register.mjs +3 -3
- package/dist/setup/setup-agent-choice.mjs +10 -9
- package/dist/setup/setup-bind.mjs +2 -2
- package/package.json +1 -1
|
@@ -3,7 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PI_PROVIDER_PRESETS = exports.BUNDLED_PI_VERSION = exports.piDistributionLabel = void 0;
|
|
6
|
+
exports.PI_PROVIDER_PRESETS = exports.MAX_BUILTIN_PI_API_KEY_LENGTH = exports.BUNDLED_PI_VERSION = exports.piDistributionLabel = void 0;
|
|
7
|
+
exports.isPiLoopbackHostname = isPiLoopbackHostname;
|
|
8
|
+
exports.presetIdForOfficialProvider = presetIdForOfficialProvider;
|
|
9
|
+
exports.builtinPiProviderRecoveryNextAction = builtinPiProviderRecoveryNextAction;
|
|
7
10
|
exports.validatePiBaseUrl = validatePiBaseUrl;
|
|
8
11
|
exports.listProviderModels = listProviderModels;
|
|
9
12
|
exports.resolveBuiltinPiProviderSetupSelection = resolveBuiltinPiProviderSetupSelection;
|
|
@@ -22,6 +25,8 @@ const secure_metadata_js_1 = require("../platform/secure-metadata.cjs");
|
|
|
22
25
|
var pi_distribution_label_js_1 = require("./pi-distribution-label.cjs");
|
|
23
26
|
Object.defineProperty(exports, "piDistributionLabel", { enumerable: true, get: function () { return pi_distribution_label_js_1.piDistributionLabel; } });
|
|
24
27
|
exports.BUNDLED_PI_VERSION = "0.84.2";
|
|
28
|
+
/** API Key 字符上限;stdin 必须在读入任意多余输入前按同一上限截断。 */
|
|
29
|
+
exports.MAX_BUILTIN_PI_API_KEY_LENGTH = 16_384;
|
|
25
30
|
// These values mirror the catalog shipped by the pinned official Pi distribution.
|
|
26
31
|
// Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
|
|
27
32
|
exports.PI_PROVIDER_PRESETS = Object.freeze([
|
|
@@ -58,6 +63,24 @@ exports.PI_PROVIDER_PRESETS = Object.freeze([
|
|
|
58
63
|
]);
|
|
59
64
|
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
60
65
|
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:@+\/-]{0,255}$/;
|
|
66
|
+
/** Node 的 URL.hostname 对 IPv6 是 [::1],不是 ::1。 */
|
|
67
|
+
function isPiLoopbackHostname(hostname) {
|
|
68
|
+
const host = hostname.trim().replace(/^\[|\]$/g, "").toLowerCase();
|
|
69
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
70
|
+
}
|
|
71
|
+
/** 官方 provider ID(如 zai-coding-cn)映射到 CLI/Dashboard 接受的 preset ID(如 zhipu)。 */
|
|
72
|
+
function presetIdForOfficialProvider(providerId) {
|
|
73
|
+
const id = providerId.trim();
|
|
74
|
+
if (id === "larkin-custom")
|
|
75
|
+
return "custom";
|
|
76
|
+
return exports.PI_PROVIDER_PRESETS.find((preset) => preset.provider === id)?.id ?? null;
|
|
77
|
+
}
|
|
78
|
+
/** Agent 范围的缺凭证恢复路径:只用 preset,不指向 setup / import / 全局 ~/.pi。 */
|
|
79
|
+
function builtinPiProviderRecoveryNextAction(options = {}) {
|
|
80
|
+
const preset = options.providerId ? presetIdForOfficialProvider(options.providerId) ?? "<preset|custom>" : "<preset|custom>";
|
|
81
|
+
const agent = options.agentId && /^cli_[A-Za-z0-9]+$/.test(options.agentId) ? options.agentId : "<App ID>";
|
|
82
|
+
return `在 Dashboard 的 Provider Credentials 中为该 Agent 配置 Provider 与 API Key,或运行 larkin pi-auth login ${preset} --agent ${agent}。`;
|
|
83
|
+
}
|
|
61
84
|
function validatePiBaseUrl(raw) {
|
|
62
85
|
const input = raw.trim();
|
|
63
86
|
if (!input || /[\0\r\n\t]/.test(input))
|
|
@@ -73,7 +96,7 @@ function validatePiBaseUrl(raw) {
|
|
|
73
96
|
throw new Error("Base URL 不允许包含用户名或凭证");
|
|
74
97
|
if (url.search || url.hash)
|
|
75
98
|
throw new Error("Base URL 不允许 query 或 fragment");
|
|
76
|
-
const local =
|
|
99
|
+
const local = isPiLoopbackHostname(url.hostname);
|
|
77
100
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
78
101
|
throw new Error("Base URL 必须使用 https;仅 localhost/loopback 开发端点允许 http");
|
|
79
102
|
}
|
|
@@ -133,7 +156,7 @@ function resolveBuiltinPiProviderSetupSelection(input) {
|
|
|
133
156
|
}
|
|
134
157
|
function validateBuiltinPiProviderSelection(input) {
|
|
135
158
|
const key = input.apiKey.trim();
|
|
136
|
-
if (!key || key.length >
|
|
159
|
+
if (!key || key.length > exports.MAX_BUILTIN_PI_API_KEY_LENGTH || /[\0\r\n]/.test(key))
|
|
137
160
|
throw new Error("API Key 不能为空或包含控制字符");
|
|
138
161
|
return { ...resolveBuiltinPiProviderSetupSelection(input), apiKey: key };
|
|
139
162
|
}
|
|
@@ -5,6 +5,8 @@ import { internalCommandSpec } from "../app/internal-command.mjs";
|
|
|
5
5
|
import { isWindows, secureWindowsDirectoryAcl } from "../platform/secure-metadata.mjs";
|
|
6
6
|
export { piDistributionLabel } from "./pi-distribution-label.mjs";
|
|
7
7
|
export const BUNDLED_PI_VERSION = "0.84.2";
|
|
8
|
+
/** API Key 字符上限;stdin 必须在读入任意多余输入前按同一上限截断。 */
|
|
9
|
+
export const MAX_BUILTIN_PI_API_KEY_LENGTH = 16_384;
|
|
8
10
|
// These values mirror the catalog shipped by the pinned official Pi distribution.
|
|
9
11
|
// Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
|
|
10
12
|
export const PI_PROVIDER_PRESETS = Object.freeze([
|
|
@@ -41,6 +43,24 @@ export const PI_PROVIDER_PRESETS = Object.freeze([
|
|
|
41
43
|
]);
|
|
42
44
|
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
43
45
|
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:@+\/-]{0,255}$/;
|
|
46
|
+
/** Node 的 URL.hostname 对 IPv6 是 [::1],不是 ::1。 */
|
|
47
|
+
export function isPiLoopbackHostname(hostname) {
|
|
48
|
+
const host = hostname.trim().replace(/^\[|\]$/g, "").toLowerCase();
|
|
49
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
50
|
+
}
|
|
51
|
+
/** 官方 provider ID(如 zai-coding-cn)映射到 CLI/Dashboard 接受的 preset ID(如 zhipu)。 */
|
|
52
|
+
export function presetIdForOfficialProvider(providerId) {
|
|
53
|
+
const id = providerId.trim();
|
|
54
|
+
if (id === "larkin-custom")
|
|
55
|
+
return "custom";
|
|
56
|
+
return PI_PROVIDER_PRESETS.find((preset) => preset.provider === id)?.id ?? null;
|
|
57
|
+
}
|
|
58
|
+
/** Agent 范围的缺凭证恢复路径:只用 preset,不指向 setup / import / 全局 ~/.pi。 */
|
|
59
|
+
export function builtinPiProviderRecoveryNextAction(options = {}) {
|
|
60
|
+
const preset = options.providerId ? presetIdForOfficialProvider(options.providerId) ?? "<preset|custom>" : "<preset|custom>";
|
|
61
|
+
const agent = options.agentId && /^cli_[A-Za-z0-9]+$/.test(options.agentId) ? options.agentId : "<App ID>";
|
|
62
|
+
return `在 Dashboard 的 Provider Credentials 中为该 Agent 配置 Provider 与 API Key,或运行 larkin pi-auth login ${preset} --agent ${agent}。`;
|
|
63
|
+
}
|
|
44
64
|
export function validatePiBaseUrl(raw) {
|
|
45
65
|
const input = raw.trim();
|
|
46
66
|
if (!input || /[\0\r\n\t]/.test(input))
|
|
@@ -56,7 +76,7 @@ export function validatePiBaseUrl(raw) {
|
|
|
56
76
|
throw new Error("Base URL 不允许包含用户名或凭证");
|
|
57
77
|
if (url.search || url.hash)
|
|
58
78
|
throw new Error("Base URL 不允许 query 或 fragment");
|
|
59
|
-
const local =
|
|
79
|
+
const local = isPiLoopbackHostname(url.hostname);
|
|
60
80
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
|
|
61
81
|
throw new Error("Base URL 必须使用 https;仅 localhost/loopback 开发端点允许 http");
|
|
62
82
|
}
|
|
@@ -116,7 +136,7 @@ export function resolveBuiltinPiProviderSetupSelection(input) {
|
|
|
116
136
|
}
|
|
117
137
|
export function validateBuiltinPiProviderSelection(input) {
|
|
118
138
|
const key = input.apiKey.trim();
|
|
119
|
-
if (!key || key.length >
|
|
139
|
+
if (!key || key.length > MAX_BUILTIN_PI_API_KEY_LENGTH || /[\0\r\n]/.test(key))
|
|
120
140
|
throw new Error("API Key 不能为空或包含控制字符");
|
|
121
141
|
return { ...resolveBuiltinPiProviderSetupSelection(input), apiKey: key };
|
|
122
142
|
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { PI_PROVIDER_PRESETS, configureBuiltinPiProviderModel, validateBuiltinPiProviderSelection, } from "./pi-provider-config.mjs";
|
|
2
|
+
import { beginBuiltinPiCredentialTransaction, createOfficialPiAuthInteraction, createOfficialPiLogoutRuntime, createOfficialPiModelRuntime, logoutOfficialPiProvider, runOfficialPiLogin, } from "./pi-official-auth.mjs";
|
|
3
|
+
import { loadConfig, markConfigApplied, mutateConfig, runtimeConfigSignature, } from "../platform/config.mjs";
|
|
4
|
+
import { requestAgentUpsert } from "../app/local-control.mjs";
|
|
5
|
+
import { readProcessState } from "../platform/process-state.mjs";
|
|
6
|
+
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
7
|
+
const PROVIDER_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
8
|
+
const PRESET_IDS = new Set([...PI_PROVIDER_PRESETS.map((preset) => preset.id), "custom"]);
|
|
9
|
+
/** 用户可见 catalog:已知 preset + 自定义 OpenAI 兼容端点,不含 Base URL / secret。 */
|
|
10
|
+
export function listBuiltinPiProviderCatalog() {
|
|
11
|
+
return [
|
|
12
|
+
...PI_PROVIDER_PRESETS.map((preset) => ({
|
|
13
|
+
id: preset.id,
|
|
14
|
+
name: preset.name,
|
|
15
|
+
provider: preset.provider,
|
|
16
|
+
defaultModel: `${preset.provider}/${preset.defaultModel}`,
|
|
17
|
+
custom: false,
|
|
18
|
+
openaiCompatible: preset.api === "openai-completions",
|
|
19
|
+
})),
|
|
20
|
+
{
|
|
21
|
+
id: "custom",
|
|
22
|
+
name: "Custom OpenAI-compatible",
|
|
23
|
+
provider: "larkin-custom",
|
|
24
|
+
defaultModel: null,
|
|
25
|
+
custom: true,
|
|
26
|
+
openaiCompatible: true,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
export function sanitizeProviderLoginError(error, secret) {
|
|
31
|
+
let text = error instanceof Error ? error.message : String(error);
|
|
32
|
+
if (secret) {
|
|
33
|
+
let from = 0;
|
|
34
|
+
while (from < text.length) {
|
|
35
|
+
const at = text.indexOf(secret, from);
|
|
36
|
+
if (at < 0)
|
|
37
|
+
break;
|
|
38
|
+
text = `${text.slice(0, at)}[redacted]${text.slice(at + secret.length)}`;
|
|
39
|
+
from = at + 10;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return text
|
|
43
|
+
.replace(/\b(?:authorization|proxy-authorization|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|secret|password)\s*[:=]\s*\S+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
|
|
44
|
+
.replace(/Bearer\s+\S+/gi, "Bearer [redacted]")
|
|
45
|
+
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
|
|
46
|
+
.replace(/https?:\/\/[^\s]+/gi, (url) => {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = new URL(url);
|
|
49
|
+
parsed.username = "";
|
|
50
|
+
parsed.password = "";
|
|
51
|
+
parsed.search = "";
|
|
52
|
+
parsed.hash = "";
|
|
53
|
+
return parsed.toString();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return "(url)";
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.replace(/[\0\r\n\t]+/g, " ")
|
|
60
|
+
.replace(/\s+/g, " ")
|
|
61
|
+
.trim()
|
|
62
|
+
.slice(0, 240) || "builtin Pi provider configuration failed";
|
|
63
|
+
}
|
|
64
|
+
export function resolveBuiltinPiLoginSelection(input) {
|
|
65
|
+
if (!PRESET_IDS.has(input.preset))
|
|
66
|
+
throw new Error(`未知 Pi provider preset: ${input.preset}`);
|
|
67
|
+
const preset = input.preset;
|
|
68
|
+
if (preset === "custom") {
|
|
69
|
+
if (!input.baseUrl?.trim())
|
|
70
|
+
throw new Error("custom OpenAI-compatible endpoint 需要 Base URL");
|
|
71
|
+
if (!input.model?.trim())
|
|
72
|
+
throw new Error("custom OpenAI-compatible endpoint 需要 model");
|
|
73
|
+
return validateBuiltinPiProviderSelection({
|
|
74
|
+
distribution: "builtin",
|
|
75
|
+
preset,
|
|
76
|
+
apiKey: input.apiKey,
|
|
77
|
+
model: input.model,
|
|
78
|
+
baseUrl: input.baseUrl,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (input.baseUrl?.trim())
|
|
82
|
+
throw new Error("已知 provider 不接受 --base-url;自定义端点请使用 custom");
|
|
83
|
+
const known = PI_PROVIDER_PRESETS.find((candidate) => candidate.id === preset);
|
|
84
|
+
if (!known)
|
|
85
|
+
throw new Error(`未知 Pi provider preset: ${input.preset}`);
|
|
86
|
+
return validateBuiltinPiProviderSelection({
|
|
87
|
+
distribution: "builtin",
|
|
88
|
+
preset,
|
|
89
|
+
apiKey: input.apiKey,
|
|
90
|
+
model: input.model?.trim() || known.defaultModel,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function createApiKeyOnlyInteraction(apiKey, options = {}) {
|
|
94
|
+
const questioner = {
|
|
95
|
+
ask: async () => {
|
|
96
|
+
throw new Error("API-key login does not accept interactive text prompts");
|
|
97
|
+
},
|
|
98
|
+
secret: async () => apiKey,
|
|
99
|
+
};
|
|
100
|
+
return createOfficialPiAuthInteraction({
|
|
101
|
+
questioner,
|
|
102
|
+
report: options.report ?? (() => { }),
|
|
103
|
+
signal: options.signal,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function assertTargetBuiltinPi(env, agentId, load) {
|
|
107
|
+
if (!APP_ID.test(agentId))
|
|
108
|
+
throw new Error("请用 --agent <App ID> 选择 Agent");
|
|
109
|
+
const loaded = load(env);
|
|
110
|
+
const agent = loaded.config.agents[agentId];
|
|
111
|
+
if (!agent)
|
|
112
|
+
throw new Error(`Agent ${agentId} 不存在`);
|
|
113
|
+
if (agent.runtime !== "pi" || agent.piDistribution !== "builtin") {
|
|
114
|
+
throw new Error(`Agent ${agentId} 不是内置 Pi`);
|
|
115
|
+
}
|
|
116
|
+
return { configDir: loaded.configDir };
|
|
117
|
+
}
|
|
118
|
+
async function applyTargetModel(env, agentId, model, mutate, inspect, upsert, markApplied, secret) {
|
|
119
|
+
let mutated;
|
|
120
|
+
try {
|
|
121
|
+
mutated = mutate(env, { kind: "set-agent-model", agentId, model, requireBuiltinPi: true }, { kind: "user" });
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw new Error(sanitizeProviderLoginError(error, secret));
|
|
125
|
+
}
|
|
126
|
+
const stillBuiltin = mutated.config.agents[agentId]?.runtime === "pi"
|
|
127
|
+
&& mutated.config.agents[agentId]?.piDistribution === "builtin";
|
|
128
|
+
if (!stillBuiltin) {
|
|
129
|
+
return { applyState: "pending", applyError: sanitizeProviderLoginError(new Error(`Agent ${agentId} 不是内置 Pi`), secret) };
|
|
130
|
+
}
|
|
131
|
+
const expectedSignature = runtimeConfigSignature(mutated.config, agentId);
|
|
132
|
+
const state = inspect(mutated.config.larkinHome);
|
|
133
|
+
if (state.daemon.state !== "owned" || state.supervisor.state !== "owned") {
|
|
134
|
+
return { applyState: "saved_not_applied" };
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const result = await upsert({
|
|
138
|
+
larkinHome: mutated.config.larkinHome,
|
|
139
|
+
agentId,
|
|
140
|
+
expectedSignature,
|
|
141
|
+
});
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
return {
|
|
144
|
+
applyState: "pending",
|
|
145
|
+
applyError: sanitizeProviderLoginError(result.error || "targeted apply failed", secret),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
markApplied(env, agentId, expectedSignature);
|
|
149
|
+
return { applyState: "applied" };
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return { applyState: "pending", applyError: sanitizeProviderLoginError(error, secret) };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 为已有 builtin Pi Agent 配置已知 preset 或自定义 OpenAI 兼容端点。
|
|
157
|
+
* 凭证事务覆盖 auth.json / models.json / models-store.json;模型写入走独立 CAS。
|
|
158
|
+
* 不读取 host ~/.pi,也不验证真实 provider。
|
|
159
|
+
*/
|
|
160
|
+
export async function configureBuiltinPiProvider(input, deps = {}) {
|
|
161
|
+
const env = input.env ?? process.env;
|
|
162
|
+
const load = deps.loadConfig ?? loadConfig;
|
|
163
|
+
const beginTransaction = deps.beginTransaction ?? beginBuiltinPiCredentialTransaction;
|
|
164
|
+
const configureModel = deps.configureModel ?? configureBuiltinPiProviderModel;
|
|
165
|
+
const createRuntime = deps.createRuntime ?? createOfficialPiModelRuntime;
|
|
166
|
+
const runLogin = deps.runLogin ?? runOfficialPiLogin;
|
|
167
|
+
const createInteraction = deps.createInteraction ?? createApiKeyOnlyInteraction;
|
|
168
|
+
const mutate = deps.mutateConfig ?? mutateConfig;
|
|
169
|
+
const inspect = deps.readProcessState ?? readProcessState;
|
|
170
|
+
const upsert = deps.requestUpsert ?? requestAgentUpsert;
|
|
171
|
+
const markApplied = deps.markApplied ?? markConfigApplied;
|
|
172
|
+
let selection;
|
|
173
|
+
try {
|
|
174
|
+
selection = resolveBuiltinPiLoginSelection({
|
|
175
|
+
preset: input.preset,
|
|
176
|
+
apiKey: input.apiKey,
|
|
177
|
+
model: input.model,
|
|
178
|
+
baseUrl: input.baseUrl,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
throw new Error(sanitizeProviderLoginError(error, input.apiKey));
|
|
183
|
+
}
|
|
184
|
+
const { configDir } = assertTargetBuiltinPi(env, input.agentId, load);
|
|
185
|
+
const transaction = beginTransaction(configDir, input.agentId);
|
|
186
|
+
try {
|
|
187
|
+
if (selection.preset === "custom") {
|
|
188
|
+
configureModel(configDir, input.agentId, {
|
|
189
|
+
distribution: "builtin",
|
|
190
|
+
preset: "custom",
|
|
191
|
+
model: selection.model,
|
|
192
|
+
baseUrl: selection.baseUrl,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const runtime = await createRuntime(configDir, input.agentId);
|
|
196
|
+
await runLogin(runtime, selection.provider, "api_key", createInteraction(selection.apiKey));
|
|
197
|
+
const applied = await applyTargetModel(env, input.agentId, selection.model, mutate, inspect, upsert, markApplied, input.apiKey);
|
|
198
|
+
transaction.commit();
|
|
199
|
+
return {
|
|
200
|
+
agentId: input.agentId,
|
|
201
|
+
provider: selection.provider,
|
|
202
|
+
preset: selection.preset,
|
|
203
|
+
model: selection.model,
|
|
204
|
+
credentialType: "api_key",
|
|
205
|
+
applyState: applied.applyState,
|
|
206
|
+
...(applied.applyError ? { applyError: applied.applyError } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
try {
|
|
211
|
+
transaction.rollback();
|
|
212
|
+
}
|
|
213
|
+
catch { /* 原始错误优先 */ }
|
|
214
|
+
throw new Error(sanitizeProviderLoginError(error, input.apiKey));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export async function logoutBuiltinPiProvider(input, deps = {}) {
|
|
218
|
+
const env = input.env ?? process.env;
|
|
219
|
+
const load = deps.loadConfig ?? loadConfig;
|
|
220
|
+
if (!PROVIDER_ID.test(input.providerId))
|
|
221
|
+
throw new Error("logout 需要安全的 provider ID");
|
|
222
|
+
const { configDir } = assertTargetBuiltinPi(env, input.agentId, load);
|
|
223
|
+
const beginTransaction = deps.beginTransaction ?? beginBuiltinPiCredentialTransaction;
|
|
224
|
+
const createLogoutRuntime = deps.createLogoutRuntime ?? createOfficialPiLogoutRuntime;
|
|
225
|
+
const logout = deps.logout ?? logoutOfficialPiProvider;
|
|
226
|
+
const transaction = beginTransaction(configDir, input.agentId);
|
|
227
|
+
try {
|
|
228
|
+
const runtime = await createLogoutRuntime(configDir, input.agentId);
|
|
229
|
+
await logout(runtime, input.providerId);
|
|
230
|
+
transaction.commit();
|
|
231
|
+
return { agentId: input.agentId, provider: input.providerId };
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
try {
|
|
235
|
+
transaction.rollback();
|
|
236
|
+
}
|
|
237
|
+
catch { /* 原始错误优先 */ }
|
|
238
|
+
throw new Error(sanitizeProviderLoginError(error));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -15,7 +15,7 @@ import { extractCanonicalPiSubagentNotification, ledgerStatusFromPiNotificationS
|
|
|
15
15
|
import { resolvePiSubagentRecordWatchdogExtensionArg } from "./pi-subagent-record-watchdog-injection.mjs";
|
|
16
16
|
import { effectivePiStateDir, extractBackgroundPiSubagentDispatch } from "./pi-subagent-ledger.mjs";
|
|
17
17
|
import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
|
|
18
|
-
import { classifyRuntimePrerequisite, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
|
|
18
|
+
import { classifyPiMissingCredentialRejection, classifyRuntimePrerequisite, isBuiltinPiDistribution, missingProviderCredentialReadiness, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
|
|
19
19
|
import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
|
|
20
20
|
import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, calculatePiCompactionSettings, parsePiExecutableVersion, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
|
|
21
21
|
class EventSession {
|
|
@@ -537,7 +537,20 @@ class PiSession extends EventSession {
|
|
|
537
537
|
this.awaitingAcknowledgement.delete(input.inputId);
|
|
538
538
|
this.ownedInputIds.delete(input.inputId);
|
|
539
539
|
this.inputEpochs.delete(input.inputId);
|
|
540
|
-
|
|
540
|
+
const rawReason = error instanceof Error ? error.message : String(error);
|
|
541
|
+
const missing = isBuiltinPiDistribution(this.distribution)
|
|
542
|
+
? classifyPiMissingCredentialRejection(rawReason)
|
|
543
|
+
: null;
|
|
544
|
+
if (missing) {
|
|
545
|
+
const classified = classifyPiProviderError({ provider: missing.provider, message: missing.diagnostic }, { distribution: this.distribution });
|
|
546
|
+
this.emit({
|
|
547
|
+
type: "input-error", inputId: input.inputId, retryable: false, willRetry: false,
|
|
548
|
+
message: classified.reason, errorCategory: classified.category, nextAction: classified.nextAction,
|
|
549
|
+
upstream: { provider: missing.provider, message: missing.diagnostic },
|
|
550
|
+
});
|
|
551
|
+
return { status: "rejected", inputId: input.inputId, retryable: false, reason: classified.reason };
|
|
552
|
+
}
|
|
553
|
+
return { status: "rejected", inputId: input.inputId, retryable: true, reason: rawReason };
|
|
541
554
|
}
|
|
542
555
|
}
|
|
543
556
|
onEvent(raw) {
|
|
@@ -664,7 +677,7 @@ class PiSession extends EventSession {
|
|
|
664
677
|
this.emitObservation("settled");
|
|
665
678
|
const owned = [...this.ownedInputIds].filter((inputId) => this.inputEpochs.get(inputId) === epoch);
|
|
666
679
|
if (error) {
|
|
667
|
-
const classified = classifyPiProviderError(error);
|
|
680
|
+
const classified = classifyPiProviderError(error, { distribution: this.distribution });
|
|
668
681
|
const retryable = classified.category === "rate_limit" || isRetryablePiProviderError(error.upstream.message);
|
|
669
682
|
for (const inputId of owned)
|
|
670
683
|
this.emit({
|
|
@@ -794,7 +807,7 @@ function piAssistantProviderError(message) {
|
|
|
794
807
|
...(typeof bodyError.type === "string" ? { type: bodyError.type } : {}),
|
|
795
808
|
};
|
|
796
809
|
}
|
|
797
|
-
export function classifyPiProviderError(error) {
|
|
810
|
+
export function classifyPiProviderError(error, scope) {
|
|
798
811
|
const upstream = "upstream" in error ? error.upstream : error;
|
|
799
812
|
const errorType = "type" in error ? error.type : undefined;
|
|
800
813
|
const reason = safeProviderText(upstream.message, "Pi provider request failed");
|
|
@@ -818,6 +831,13 @@ export function classifyPiProviderError(error) {
|
|
|
818
831
|
return {
|
|
819
832
|
category: "rate_limit", reason, nextAction: "Wait for the provider rate-limit window, then retry.",
|
|
820
833
|
};
|
|
834
|
+
const missing = isBuiltinPiDistribution(scope?.distribution)
|
|
835
|
+
? classifyPiMissingCredentialRejection(reason)
|
|
836
|
+
: null;
|
|
837
|
+
if (missing) {
|
|
838
|
+
const readiness = missingProviderCredentialReadiness("pi", missing.provider);
|
|
839
|
+
return { category: "auth", reason: readiness.reason, nextAction: readiness.nextAction };
|
|
840
|
+
}
|
|
821
841
|
if (upstream.status === 401
|
|
822
842
|
|| [...signal].some((value) => ["authentication_error", "invalid_api_key", "invalid_token", "token_expired", "unauthorized", "provider_auth_error"].includes(value))
|
|
823
843
|
|| /\bAPI key auth failed\b|\bFailed to resolve API key\b/i.test(reason)) {
|