oh-my-agent-web 0.3.0 → 0.4.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 +86 -455
- package/dist/server/approval/read-only.js +107 -0
- package/dist/server/approval/rules.js +85 -0
- package/dist/server/approval/scope.js +150 -0
- package/dist/server/brand.js +2 -0
- package/dist/server/cli-config.js +52 -0
- package/dist/server/cli-info.js +101 -0
- package/dist/server/cli.js +304 -0
- package/dist/server/engine/builtin.js +61 -0
- package/dist/server/engine/catalog.js +8 -0
- package/dist/server/engine/codex-approvals.js +45 -0
- package/dist/server/engine/codex-map.js +298 -0
- package/dist/server/engine/codex-sessions.js +159 -0
- package/dist/server/engine/codex-transport.js +49 -0
- package/dist/server/engine/codex.js +314 -0
- package/dist/server/engine/dsh-history.js +195 -0
- package/dist/server/engine/dsh-log.js +245 -0
- package/dist/server/engine/dsh-map.js +221 -0
- package/dist/server/engine/dsh-sessions.js +207 -0
- package/dist/server/engine/dsh-transport.js +37 -0
- package/dist/server/engine/dsh.js +284 -0
- package/dist/server/engine/fake.js +231 -0
- package/dist/server/engine/jsonrpc-stdio.js +241 -0
- package/dist/server/engine/model-registry-reload.js +115 -0
- package/dist/server/engine/pi-events.js +148 -0
- package/dist/server/engine/pi-rename.js +14 -0
- package/dist/server/engine/pi-sdk.js +343 -0
- package/dist/server/engine/pi-session-model.js +23 -0
- package/dist/server/engine/pi-thinking.js +34 -0
- package/dist/server/engine/pi-transcript.js +225 -0
- package/dist/server/engine/pi-version.js +11 -0
- package/dist/server/engine/port.js +1 -0
- package/dist/server/engine/registry.js +47 -0
- package/dist/server/engine/scratch.js +13 -0
- package/dist/server/engine/suggestion-guidance.js +9 -0
- package/dist/server/engine/switchable.js +22 -0
- package/dist/server/engine/wiring.js +107 -0
- package/dist/server/files/dir-routes.js +40 -0
- package/dist/server/files/file-search.js +63 -0
- package/dist/server/files/file-service.js +232 -0
- package/dist/server/files/files-routes.js +182 -0
- package/dist/server/files/git-info.js +186 -0
- package/dist/server/http/auth-routes.js +99 -0
- package/dist/server/http/auth.js +191 -0
- package/dist/server/http/aux-routes.js +24 -0
- package/dist/server/http/compress.js +75 -0
- package/dist/server/http/health-and-static.js +33 -0
- package/dist/server/http/live-routes.js +113 -0
- package/dist/server/http/login-page.js +200 -0
- package/dist/server/index.js +342 -520
- package/dist/server/markers/actions.js +262 -0
- package/dist/server/protocol-parse.js +201 -0
- package/dist/server/protocol-settings.js +1 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/providers/catalog-store.js +85 -0
- package/dist/server/providers/catalog-writes.js +197 -0
- package/dist/server/providers/catalog.js +77 -0
- package/dist/server/providers/enabled-models.js +118 -0
- package/dist/server/providers/import-models.js +69 -0
- package/dist/server/providers/import-routes.js +136 -0
- package/dist/server/providers/model-test.js +52 -0
- package/dist/server/providers/probe.js +64 -0
- package/dist/server/providers/provider-handlers.js +203 -0
- package/dist/server/providers/provider-store.js +327 -0
- package/dist/server/providers/request-headers.js +7 -0
- package/dist/server/providers/validate.js +241 -0
- package/dist/server/push/push-routes.js +114 -0
- package/dist/server/push/push.js +120 -0
- package/dist/server/session/approval-bridge.js +103 -0
- package/dist/server/session/attachment-resolve.js +21 -0
- package/dist/server/session/client-view.js +82 -0
- package/dist/server/session/conversation-create.js +65 -0
- package/dist/server/session/delete-session.js +84 -0
- package/dist/server/session/engine-switch.js +108 -0
- package/dist/server/session/lock.js +82 -0
- package/dist/server/session/model-controller.js +111 -0
- package/dist/server/session/notices.js +31 -0
- package/dist/server/session/pi-opener.js +123 -0
- package/dist/server/session/pi-refresh.js +51 -0
- package/dist/server/session/pi-sessions-provider.js +46 -0
- package/dist/server/session/pi-sessions.js +272 -0
- package/dist/server/session/refresh-actions.js +42 -0
- package/dist/server/session/rename-session.js +23 -0
- package/dist/server/session/runtime-services.js +28 -0
- package/dist/server/session/runtime-types.js +1 -0
- package/dist/server/session/runtime.js +358 -0
- package/dist/server/session/session-stats.js +88 -0
- package/dist/server/session/settings.js +367 -0
- package/dist/server/session/snapshot-for.js +7 -0
- package/dist/server/session/stats-refresher.js +50 -0
- package/dist/server/session/switch-conversation.js +86 -0
- package/dist/server/session/throttle.js +14 -0
- package/dist/server/session/turn-events.js +126 -0
- package/dist/server/session/turn-finish.js +79 -0
- package/dist/server/session/turn-markers.js +26 -0
- package/dist/server/session/turn-queue.js +111 -0
- package/dist/server/session/turn-send.js +88 -0
- package/dist/server/session/turn-watchdog.js +77 -0
- package/dist/server/sessions/session-routes.js +17 -0
- package/dist/server/skills/skill-routes.js +28 -0
- package/dist/server/skills/skill-store.js +134 -0
- package/dist/server/snapshot/fan-out.js +10 -0
- package/dist/server/snapshot/messages.js +75 -0
- package/dist/server/snapshot/sender.js +62 -0
- package/dist/server/snapshot/state.js +193 -0
- package/dist/server/terminal/terminal-routes.js +176 -0
- package/dist/server/terminal/terminal-session.js +140 -0
- package/dist/server/uploads/upload-routes.js +64 -0
- package/dist/server/uploads/upload-store.js +115 -0
- package/dist/server/util/data-dir.js +36 -0
- package/dist/server/util/env.js +140 -0
- package/dist/server/util/ids.js +28 -0
- package/dist/server/util/log-file.js +67 -0
- package/dist/server/util/log.js +52 -0
- package/dist/server/util/origin.js +75 -0
- package/dist/server/util/paths.js +60 -0
- package/dist/server/util/pricing-index.js +121 -0
- package/dist/server/util/pricing.js +214 -0
- package/dist/server/util/startup.js +34 -0
- package/dist/server/util/upstream-version.js +140 -0
- package/dist/server/util/usage.js +19 -0
- package/dist/server/util/version.js +50 -0
- package/dist/server/ws/handle-message.js +95 -0
- package/package.json +49 -63
- package/web/dist/apple-touch-icon-precomposed.png +0 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/abnfDiagram-WEYOIA3C-NLp09iEC.js +1 -0
- package/web/dist/assets/arc-DviL9o3c.js +1 -0
- package/web/dist/assets/architecture-WOLXFQ4H-fjsM_Sy3.js +1 -0
- package/web/dist/assets/architectureDiagram-ZHVYKT53-Cn3B4TE8.js +36 -0
- package/web/dist/assets/array-BifhSqXX.js +1 -0
- package/web/dist/assets/blockDiagram-RJAE7QA3-CIuvxWvi.js +140 -0
- package/web/dist/assets/c4Diagram-XDPPPE54-DvdKmEpF.js +38 -0
- package/web/dist/assets/channel-C3AQ6nAs.js +1 -0
- package/web/dist/assets/chunk-2E7WHVMD-HNNASGCS.js +88 -0
- package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web/dist/assets/chunk-4EMNKR2T-Cg-eRwBE.js +1 -0
- package/web/dist/assets/chunk-55XCU76K-BweiisZL.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/web/dist/assets/chunk-7S6XSTFO-BCzIiP-6.js +2 -0
- package/web/dist/assets/chunk-AQDTSKZB-Dl0NRFuU.js +72 -0
- package/web/dist/assets/chunk-DSNLSHJZ-zhHCjjN9.js +129 -0
- package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/web/dist/assets/chunk-HAGFOTED-BDQqU2EH.js +62 -0
- package/web/dist/assets/chunk-HNE4Q36P-B-qRzMjX.js +1 -0
- package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/web/dist/assets/chunk-NQOKRVIF-CNcvhMn_.js +213 -0
- package/web/dist/assets/chunk-UHW7PFBP-Dwr8LUzr.js +1 -0
- package/web/dist/assets/chunk-VQ5W62QI-O8sN6jJu.js +1 -0
- package/web/dist/assets/chunk-WQQL4CDK-B6-59Zg2.js +10 -0
- package/web/dist/assets/chunk-XXDRQBXY-DvSbeUlZ.js +1 -0
- package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/web/dist/assets/chunk-YZO67HFG-BXpKYKsw.js +1 -0
- package/web/dist/assets/chunk-ZAUX2LTX-B1Z5VSkz.js +1 -0
- package/web/dist/assets/chunk-ZCHMQSOR-h_5mBLIA.js +2 -0
- package/web/dist/assets/classDiagram-v2-4MYWR3UA-Dr6JnaaV.js +217 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-36ShkR7R.js +1 -0
- package/web/dist/assets/cynefin-EF2NZ3EQ-Dg6bI7pC.js +1 -0
- package/web/dist/assets/cynefinDiagram-WF24UDRP-B0YHpBlR.js +62 -0
- package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/web/dist/assets/dagre-NK4GIZVX-Cx3mn7YS.js +4 -0
- package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/web/dist/assets/diagram-CBK3MWTC-l8aZG_Bm.js +24 -0
- package/web/dist/assets/diagram-I5XIIV2D-Bd5f14_h.js +24 -0
- package/web/dist/assets/diagram-ODIL2UCW-hBsxCYUj.js +30 -0
- package/web/dist/assets/diagram-PFBEJUXE-BgpIBUYl.js +3 -0
- package/web/dist/assets/diagram-RJA3I4VV-CDU1gUVD.js +41 -0
- package/web/dist/assets/diagram-TYUNFSNR-Iuxh7Tqf.js +200 -0
- package/web/dist/assets/dist-C86cKTuE.js +1 -0
- package/web/dist/assets/ebnfDiagram-V4P3TTPT-Cms0HFKh.js +1 -0
- package/web/dist/assets/elk-QONTBCTQ-B7yEy0PQ.js +27 -0
- package/web/dist/assets/erDiagram-KYQIGQAA-CL48j4aU.js +99 -0
- package/web/dist/assets/eventmodeling-K75KTNOO-CFvkaejh.js +1 -0
- package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +1 -0
- package/web/dist/assets/ganttDiagram-R7GPNTJY-vdMr-0ZV.js +292 -0
- package/web/dist/assets/gitGraph-VSP46ZUC-2z85Wuwo.js +1 -0
- package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BY4GPiqC.js +106 -0
- package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/web/dist/assets/index-Bjn56a2i.js +76 -0
- package/web/dist/assets/index-Drp6e5iU.css +1 -0
- package/web/dist/assets/info-OHQRW6UA-ECzkYTx-.js +1 -0
- package/web/dist/assets/infoDiagram-MVDRKAX5-DDk5EAeb.js +2 -0
- package/web/dist/assets/init-C-OQMol4.js +1 -0
- package/web/dist/assets/ishikawaDiagram-HPEHV2GE-9yPH4J94.js +70 -0
- package/web/dist/assets/journeyDiagram-BOSHW7O7-BPTIap3l.js +139 -0
- package/web/dist/assets/kanban-definition-FNSA4HC5-BbADHrA8.js +89 -0
- package/web/dist/assets/katex-B_JcIwqj.js +257 -0
- package/web/dist/assets/line-CHV9_4lL.js +1 -0
- package/web/dist/assets/linear-C-eZsBTv.js +1 -0
- package/web/dist/assets/mermaid-parser.core-BiAhX1l1.js +7 -0
- package/web/dist/assets/mermaid.core-BxP_A_zj.js +44 -0
- package/web/dist/assets/mindmap-definition-6OY57UG3-BMWlit95.js +98 -0
- package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/web/dist/assets/packet-JDAUHWVQ-FVApuKOp.js +1 -0
- package/web/dist/assets/path-fybaL0A-.js +1 -0
- package/web/dist/assets/pegDiagram-ECIUJNBX-WjWJvDHM.js +1 -0
- package/web/dist/assets/pie-XZMESJXO-BGCZnvhp.js +1 -0
- package/web/dist/assets/pieDiagram-GNMPY74T-DJpAC_JC.js +39 -0
- package/web/dist/assets/quadrantDiagram-D57K4MC3-hxx5c7oq.js +7 -0
- package/web/dist/assets/radar-ABXABTNO-BU9Shc-O.js +1 -0
- package/web/dist/assets/railroad-I3PHUGI6-pWj8d-G4.js +1 -0
- package/web/dist/assets/railroad-abnf-LNEFI6M7-D7HZBTAj.js +1 -0
- package/web/dist/assets/railroad-ebnf-DWYD2UWJ-CKiXJSuM.js +1 -0
- package/web/dist/assets/railroad-peg-JR7BVNR7-DB3VJHgR.js +1 -0
- package/web/dist/assets/railroadDiagram-Y72NRBL7-Cev7Y65k.js +1 -0
- package/web/dist/assets/requirementDiagram-EQYSW5RA-CqbwMLD8.js +84 -0
- package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/web/dist/assets/sankeyDiagram-SY24JHO5-V2cyVi8q.js +40 -0
- package/web/dist/assets/sequenceDiagram-BLEHNHQU-By6NJOPM.js +169 -0
- package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/web/dist/assets/src-Da91fG3g.js +1 -0
- package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnPGX5UI.js +291 -0
- package/web/dist/assets/swimlanes-B3ENRKFQ-C58BpIpm.js +1 -0
- package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +8 -0
- package/web/dist/assets/timeline-definition-JXYKGLGB-Hgo1DhnS.js +120 -0
- package/web/dist/assets/treeView-D4JQ5SDB-DF0dfjqB.js +1 -0
- package/web/dist/assets/treemap-SAJKECNS-CSSM5FqB.js +1 -0
- package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkBBD1NY.js +328 -0
- package/web/dist/assets/vennDiagram-Q3Y3IDCW-Bm28keX_.js +34 -0
- package/web/dist/assets/wardley-7MLQ67FV-By7KgEQX.js +1 -0
- package/web/dist/assets/wardleyDiagram-IVAIDKWA-oVtIrnGa.js +78 -0
- package/web/dist/assets/xychartDiagram-4URNHDU4-aI8FH4g2.js +7 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/favicon.svg +20 -8
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-maskable-512.png +0 -0
- package/web/dist/index.html +21 -27
- package/web/dist/manifest.webmanifest +17 -0
- package/web/dist/maskable.svg +25 -0
- package/web/dist/sw.js +40 -0
- package/bin/oh-my-agent-web-ps.mjs +0 -203
- package/bin/oh-my-agent-web-ws.mjs +0 -782
- package/bin/oh-my-agent-web.mjs +0 -321
- package/bin/update-check.mjs +0 -142
- package/dist/server/codex-client.js +0 -310
- package/dist/server/codex-supervisor.js +0 -446
- package/dist/server/fs-service.js +0 -144
- package/dist/server/request-inspector.js +0 -458
- package/dist/shared/protocol.js +0 -112
- package/scripts/manage-server.mjs +0 -155
- package/scripts/omaw.mjs +0 -74
- package/shared/protocol.ts +0 -228
- package/web/dist/assets/index-5LJ5fRcO.js +0 -42
- package/web/dist/assets/index-QiDfPKFo.css +0 -1
- package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { testModel } from "./model-test.js";
|
|
2
|
+
import { providerHeaders } from "./request-headers.js";
|
|
3
|
+
export async function probeProvider(options) {
|
|
4
|
+
let url;
|
|
5
|
+
try {
|
|
6
|
+
url = new URL(`${options.baseUrl.replace(/\/+$/, "")}/models`);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return { ok: false, message: "baseUrl 不是合法 URL" };
|
|
10
|
+
}
|
|
11
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
12
|
+
return { ok: false, message: "只允许 http(s)" };
|
|
13
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
14
|
+
const started = Date.now();
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 8000);
|
|
17
|
+
try {
|
|
18
|
+
const response = await doFetch(url.toString(), {
|
|
19
|
+
method: "GET",
|
|
20
|
+
headers: providerHeaders(options.api ?? "openai-completions", options.apiKey, options.headers),
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
23
|
+
const latencyMs = Date.now() - started;
|
|
24
|
+
if (response.ok)
|
|
25
|
+
return { ok: true, status: response.status, latencyMs };
|
|
26
|
+
// 401/403 说明地址通但 key 不对;其余按状态码报
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
message: `${response.status}${response.status === 401 || response.status === 403 ? "(地址可达,key 可能不对)" : ""}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return { ok: false, message: /abort/i.test(message) ? "超时(8 秒内没有响应)" : `请求失败:${url.host}` };
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function verifyProviderConfig(options) {
|
|
41
|
+
const probe = await probeProvider({
|
|
42
|
+
baseUrl: options.baseUrl,
|
|
43
|
+
api: options.api,
|
|
44
|
+
apiKey: options.apiKey,
|
|
45
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
46
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
47
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
48
|
+
});
|
|
49
|
+
if (!probe.ok)
|
|
50
|
+
return { ok: false, step: "models", message: probe.message };
|
|
51
|
+
if (!options.modelId)
|
|
52
|
+
return { ok: true, status: probe.status ?? 200, latencyMs: probe.latencyMs };
|
|
53
|
+
const chat = await testModel({
|
|
54
|
+
baseUrl: options.baseUrl,
|
|
55
|
+
api: options.api,
|
|
56
|
+
apiKey: options.apiKey,
|
|
57
|
+
modelId: options.modelId,
|
|
58
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
59
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
60
|
+
});
|
|
61
|
+
if (!chat.ok)
|
|
62
|
+
return { ok: false, step: "chat", message: chat.message };
|
|
63
|
+
return { ok: true, status: probe.status ?? 200, latencyMs: probe.latencyMs, modelLatencyMs: chat.latencyMs };
|
|
64
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { validateModelSpec, validateProviderInput } from "./validate.js";
|
|
2
|
+
import { toggleModel, toggleProvider } from "./enabled-models.js";
|
|
3
|
+
/** 返回 null = 不是 provider 相关请求(交回主分发) */
|
|
4
|
+
export async function handleProviderRequest(request, deps) {
|
|
5
|
+
if (request.type === "list_providers")
|
|
6
|
+
return { ok: true, result: { providers: await deps.store.list(deps.builtinProviders()) } };
|
|
7
|
+
if (request.type === "upsert_provider") {
|
|
8
|
+
const parsed = validateProviderInput(request.payload);
|
|
9
|
+
if (!parsed.ok) {
|
|
10
|
+
const message = parsed.errors.join(";");
|
|
11
|
+
deps.notify(`provider 配置有问题:${message}`, "error");
|
|
12
|
+
return { ok: false, error: { code: "provider.invalid", message } };
|
|
13
|
+
}
|
|
14
|
+
const result = await deps.store.upsert(parsed.value);
|
|
15
|
+
if (!result.ok) {
|
|
16
|
+
deps.notify(`provider 保存失败:${result.error}`, "error");
|
|
17
|
+
return { ok: false, error: { code: "provider.upsert_failed", message: result.error } };
|
|
18
|
+
}
|
|
19
|
+
deps.logInfo("provider 已保存", {
|
|
20
|
+
provider: result.view.provider,
|
|
21
|
+
hasKey: result.view.hasKey,
|
|
22
|
+
models: result.view.models.length,
|
|
23
|
+
});
|
|
24
|
+
const from = request.payload?.previousProvider;
|
|
25
|
+
const what = from
|
|
26
|
+
? `provider「${from}」已改名为「${result.view.provider}」`
|
|
27
|
+
: `provider「${result.view.provider}」已保存`;
|
|
28
|
+
deps.notify(`${what}:${result.view.models.length} 个模型${result.view.hasKey ? `,key ${result.view.keyHint}` : ""}(已热重载)`, "info");
|
|
29
|
+
return { ok: true, result: { provider: result.view } };
|
|
30
|
+
}
|
|
31
|
+
if (request.type === "update_model") {
|
|
32
|
+
const payload = request.payload;
|
|
33
|
+
const parsed = validateModelSpec(payload.model);
|
|
34
|
+
if (!parsed.ok) {
|
|
35
|
+
const message = parsed.errors.join(";");
|
|
36
|
+
deps.notify(`模型信息有问题:${message}`, "error");
|
|
37
|
+
return { ok: false, error: { code: "model.invalid", message } };
|
|
38
|
+
}
|
|
39
|
+
const view = await deps.store.get(payload.provider, deps.builtinProviders());
|
|
40
|
+
const seed = view?.source === "builtin"
|
|
41
|
+
? { baseUrl: view.baseUrl, api: view.api, models: view.models.map((m) => ({ id: m.id, name: m.name })) }
|
|
42
|
+
: undefined;
|
|
43
|
+
const result = await deps.store.updateModel(payload.provider, parsed.value, seed);
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
deps.notify(`保存模型失败:${result.error}`, "error");
|
|
46
|
+
return { ok: false, error: { code: "model.update_failed", message: result.error } };
|
|
47
|
+
}
|
|
48
|
+
deps.notify(`模型「${parsed.value.name}」已保存,模型表已热重载`, "info");
|
|
49
|
+
return { ok: true };
|
|
50
|
+
}
|
|
51
|
+
if (request.type === "set_enabled_models") {
|
|
52
|
+
const payload = request
|
|
53
|
+
.payload;
|
|
54
|
+
// provider === "*":全开(pi 的语义是「没有白名单 = 不限制」)
|
|
55
|
+
if (payload.provider === "*") {
|
|
56
|
+
await deps.enabled.write([]);
|
|
57
|
+
deps.notify("已启用全部模型(清空 enabledModels 白名单)", "info");
|
|
58
|
+
return { ok: true, result: { enabledModels: [] } };
|
|
59
|
+
}
|
|
60
|
+
const patterns = await deps.enabled.read();
|
|
61
|
+
const builtins = deps.builtinProviders();
|
|
62
|
+
// 内置 provider 也在名单里:否则 view 为 null,modelIds 为空,
|
|
63
|
+
// 「停用其中一个模型」会被展开成 [](= 禁用整个 provider)
|
|
64
|
+
const view = await deps.store.get(payload.provider, builtins);
|
|
65
|
+
// 全开状态下停用一个 provider,需要知道「其它 provider 有哪些模型」才能展开成白名单
|
|
66
|
+
const universe = (await deps.store.list(builtins)).map((item) => ({
|
|
67
|
+
provider: item.provider,
|
|
68
|
+
models: item.models.map((model) => model.id),
|
|
69
|
+
}));
|
|
70
|
+
const modelIds = (view?.models ?? []).map((model) => model.id);
|
|
71
|
+
const next = payload.modelId
|
|
72
|
+
? toggleModel(patterns, payload.provider, modelIds, payload.modelId, payload.enabled, universe)
|
|
73
|
+
: toggleProvider(patterns, payload.provider, modelIds, payload.enabled, universe);
|
|
74
|
+
await deps.enabled.write(next);
|
|
75
|
+
const what = payload.modelId ? `模型「${payload.modelId}」` : `provider「${payload.provider}」`;
|
|
76
|
+
deps.notify(`${what}${payload.enabled ? "已启用" : "已停用"}(写进 pi 的 settings.json)`, "info");
|
|
77
|
+
return { ok: true, result: { enabledModels: next } };
|
|
78
|
+
}
|
|
79
|
+
if (request.type === "remove_model") {
|
|
80
|
+
const payload = request.payload;
|
|
81
|
+
const view = await deps.store.get(payload.provider, deps.builtinProviders());
|
|
82
|
+
const seed = view?.source === "builtin"
|
|
83
|
+
? { baseUrl: view.baseUrl, api: view.api, models: view.models.map((m) => ({ id: m.id, name: m.name })) }
|
|
84
|
+
: undefined;
|
|
85
|
+
const result = await deps.store.removeModel(payload.provider, payload.modelId, seed);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
deps.notify(`移除模型失败:${result.error}`, "error");
|
|
88
|
+
return { ok: false, error: { code: "model.remove_failed", message: result.error } };
|
|
89
|
+
}
|
|
90
|
+
deps.notify(`已从「${payload.provider}」移除模型:${payload.modelId}`, "info");
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
if (request.type === "delete_provider") {
|
|
94
|
+
const provider = String(request.provider ?? "");
|
|
95
|
+
const result = await deps.store.remove(provider);
|
|
96
|
+
if (!result.ok) {
|
|
97
|
+
deps.notify(`删除失败:${result.error}`, "error");
|
|
98
|
+
return { ok: false, error: { code: "provider.delete_failed", message: result.error } };
|
|
99
|
+
}
|
|
100
|
+
deps.logInfo("provider 已删除", { provider, removed: result.removed });
|
|
101
|
+
deps.notify(result.removed
|
|
102
|
+
? `provider「${provider}」已删除`
|
|
103
|
+
: `provider「${provider}」不在这两份配置文件里(可能是插件注册的),无法从这里删除`, result.removed ? "info" : "warn");
|
|
104
|
+
return { ok: true, result: { removed: result.removed } };
|
|
105
|
+
}
|
|
106
|
+
if (request.type === "test_provider") {
|
|
107
|
+
const provider = String(request.provider ?? "");
|
|
108
|
+
// 内置 provider 也要能测:以前查不到直接报「没有这个 provider」
|
|
109
|
+
const view = await deps.store.get(provider, deps.builtinProviders());
|
|
110
|
+
if (!view) {
|
|
111
|
+
deps.notify(`没有这个 provider:${provider}`, "warn");
|
|
112
|
+
return { ok: false, error: { code: "provider.not_found", message: "没有这个 provider" } };
|
|
113
|
+
}
|
|
114
|
+
const apiKey = await deps.readKey(provider);
|
|
115
|
+
if (!apiKey) {
|
|
116
|
+
deps.notify(`provider「${provider}」还没有 api key,无法测试`, "warn");
|
|
117
|
+
return { ok: false, error: { code: "provider.no_key", message: "没有 api key" } };
|
|
118
|
+
}
|
|
119
|
+
const result = await deps.probe({
|
|
120
|
+
baseUrl: view.baseUrl,
|
|
121
|
+
api: view.api,
|
|
122
|
+
apiKey,
|
|
123
|
+
headers: await deps.store.readHeaders(provider),
|
|
124
|
+
});
|
|
125
|
+
deps.notify(result.ok
|
|
126
|
+
? `provider「${provider}」连通正常(HTTP ${result.status},${result.latencyMs}ms)`
|
|
127
|
+
: `provider「${provider}」连不通:${result.message}`, result.ok ? "info" : "error");
|
|
128
|
+
return { ok: true, result };
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/** 组装依赖:把「读 key / 发探测 / 日志 / 提示」这些副作用集中在一处,index 里只留一行 */
|
|
133
|
+
export function createProviderDeps(options) {
|
|
134
|
+
return {
|
|
135
|
+
store: options.store,
|
|
136
|
+
notify: options.notify,
|
|
137
|
+
logInfo: (message, meta) => options.logger.info(message, meta),
|
|
138
|
+
readKey: (provider) => options.store.readKey(provider),
|
|
139
|
+
builtinProviders: options.builtinProviders,
|
|
140
|
+
enabled: options.enabled ?? {
|
|
141
|
+
read: async () => [],
|
|
142
|
+
write: async () => { },
|
|
143
|
+
},
|
|
144
|
+
probe: options.probe,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 把「处理 → 刷新快照 → 回包」串起来(index.ts 里只留一行,见 400 行护栏)。
|
|
149
|
+
* 回包用回调传入,这样这里不需要认识 WebSocket。
|
|
150
|
+
*/
|
|
151
|
+
export function createProviderRunner(options) {
|
|
152
|
+
const deps = createProviderDeps(options);
|
|
153
|
+
return async (request, reply) => {
|
|
154
|
+
const handled = await handleProviderRequest(request, deps);
|
|
155
|
+
await options.refresh();
|
|
156
|
+
if (handled)
|
|
157
|
+
reply(handled);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** 引擎报上来的模型按 provider 前缀分组(`provider/modelId`)→「内置 provider」列表 */
|
|
161
|
+
export function builtinProviderGroups(models) {
|
|
162
|
+
const groups = new Map();
|
|
163
|
+
const endpoint = new Map();
|
|
164
|
+
for (const model of models) {
|
|
165
|
+
const slash = model.id.indexOf("/");
|
|
166
|
+
if (slash <= 0)
|
|
167
|
+
continue;
|
|
168
|
+
const provider = model.id.slice(0, slash);
|
|
169
|
+
groups.set(provider, [...(groups.get(provider) ?? []), { id: model.id.slice(slash + 1), name: model.label }]);
|
|
170
|
+
// 引擎知道内置 provider 的真实端点;没有它,「测试」按钮对内 provider 就是死的
|
|
171
|
+
if (!endpoint.has(provider) && (model.baseUrl || model.api)) {
|
|
172
|
+
endpoint.set(provider, {
|
|
173
|
+
...(model.baseUrl ? { baseUrl: model.baseUrl } : {}),
|
|
174
|
+
...(model.api ? { api: model.api } : {}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return [...groups].map(([provider, list]) => ({ provider, models: list, ...endpoint.get(provider) }));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 把 provider 的「请求处理 + 快照刷新」接线整体封装(放在这里是为了 index.ts 的 400 行护栏)。
|
|
182
|
+
* `getRuntime` 用 getter 传进来:runtime 在 index 里晚于本模块构造,只有调用时才需要它。
|
|
183
|
+
*/
|
|
184
|
+
export function createProviderWiring(options) {
|
|
185
|
+
const runtime = () => options.getRuntime();
|
|
186
|
+
return createProviderRunner({
|
|
187
|
+
store: options.store,
|
|
188
|
+
logger: options.logger,
|
|
189
|
+
probe: options.probe,
|
|
190
|
+
...(options.enabled ? { enabled: options.enabled } : {}),
|
|
191
|
+
notify: (text, level) => runtime().notify(text, level),
|
|
192
|
+
builtinProviders: () => builtinProviderGroups(runtime().state().models),
|
|
193
|
+
refresh: async () => {
|
|
194
|
+
const live = runtime().state();
|
|
195
|
+
live.providers = await options.store.list(builtinProviderGroups(live.models));
|
|
196
|
+
// 启用白名单也一起刷:否则界面的开关会停在旧状态(点了像没生效)
|
|
197
|
+
if (options.enabled)
|
|
198
|
+
live.enabledModels = await options.enabled.read();
|
|
199
|
+
await runtime().reloadModels();
|
|
200
|
+
runtime().pushSnapshot();
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider 配置的读写(ADR-0025 §1):**复用 pi 自己的两个文件**,不另存一份 key。
|
|
3
|
+
*
|
|
4
|
+
*
|
|
5
|
+
* 纪律:原子写 + 0600;**key 不出服务端**(只给 hasKey + 尾 4 位)。
|
|
6
|
+
* models.json 里的 provider 也在这里读写 —— 改它就等于改 pi 的配置。
|
|
7
|
+
*/
|
|
8
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { maskKey } from "./validate.js";
|
|
11
|
+
import { addCatalogModels, removeFromCatalog, adoptRename, writeCatalogModelPatch, writeCatalogUpsert, } from "./catalog-writes.js";
|
|
12
|
+
import { MODELS_FILE, readCatalog, specView } from "./catalog-store.js";
|
|
13
|
+
export const MODELS_STORE_FILE = "models-store.json";
|
|
14
|
+
export const AUTH_FILE = "auth.json";
|
|
15
|
+
async function readJson(path, fallback) {
|
|
16
|
+
try {
|
|
17
|
+
const text = await readFile(path, "utf8");
|
|
18
|
+
const parsed = JSON.parse(text);
|
|
19
|
+
return typeof parsed === "object" && parsed !== null ? parsed : fallback;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// 文件不存在 / 坏文件都不致命:按空表处理(与 ADR-0005 的「可重建」一致)
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** 原子写 + 0600:凭证文件不能出现「半写」也不能是 world-readable */
|
|
27
|
+
async function writeJsonPrivate(path, value) {
|
|
28
|
+
await mkdir(dirname(path), { recursive: true });
|
|
29
|
+
const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
30
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
31
|
+
await rename(temporary, path);
|
|
32
|
+
await chmod(path, 0o600);
|
|
33
|
+
}
|
|
34
|
+
/** 内置 provider → 可写条目(整表照抄,避免覆盖时丢掉别的模型) */
|
|
35
|
+
function materialize(provider, seed) {
|
|
36
|
+
if (!seed || seed.models.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
models: seed.models.map((model) => ({
|
|
40
|
+
id: model.id,
|
|
41
|
+
name: model.name,
|
|
42
|
+
// api / baseUrl 可能拿不到(引擎只报了模型名):空字符串不能写进去,
|
|
43
|
+
// 否则这个 provider 会因为「没有可用端点」被当成坏配置(模型表里也不推)
|
|
44
|
+
...(seed.api ? { api: seed.api } : {}),
|
|
45
|
+
...(seed.baseUrl ? { baseUrl: seed.baseUrl } : {}),
|
|
46
|
+
provider,
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function createProviderStore(options) {
|
|
51
|
+
const modelsPath = join(options.agentDir, MODELS_STORE_FILE);
|
|
52
|
+
const authPath = join(options.agentDir, AUTH_FILE);
|
|
53
|
+
const catalogPath = join(options.agentDir, MODELS_FILE);
|
|
54
|
+
const inCatalog = async (provider) => Boolean((await readCatalog(options.agentDir)).providers?.[provider]);
|
|
55
|
+
return {
|
|
56
|
+
async list(builtin = []) {
|
|
57
|
+
const [models, auth] = await Promise.all([
|
|
58
|
+
readJson(modelsPath, {}),
|
|
59
|
+
readJson(authPath, {}),
|
|
60
|
+
]);
|
|
61
|
+
const custom = Object.entries(models).map(([provider, entry]) => {
|
|
62
|
+
const list = Array.isArray(entry.models) ? entry.models : [];
|
|
63
|
+
const key = auth[provider]?.key;
|
|
64
|
+
const headers = list.find((model) => model.headers)?.headers;
|
|
65
|
+
return {
|
|
66
|
+
provider,
|
|
67
|
+
source: "custom",
|
|
68
|
+
baseUrl: list.find((model) => typeof model.baseUrl === "string")?.baseUrl ?? "",
|
|
69
|
+
api: list.find((model) => typeof model.api === "string")?.api ?? "",
|
|
70
|
+
hasKey: typeof key === "string" && key !== "",
|
|
71
|
+
keyHint: maskKey(typeof key === "string" ? key : undefined),
|
|
72
|
+
...(headers ? { headerNames: Object.keys(headers) } : {}),
|
|
73
|
+
models: list.map(specView),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
// pi 的 provider 目录(models.json):nexita / nexita-anthropic 这类就写在这里。
|
|
77
|
+
// 之前只从 models-store.json 读,于是它们在界面上「查无此 provider」。
|
|
78
|
+
const catalog = await readJson(catalogPath, {});
|
|
79
|
+
const fromCatalog = Object.entries(catalog.providers ?? {})
|
|
80
|
+
.filter(([provider]) => !models[provider])
|
|
81
|
+
.map(([provider, entry]) => {
|
|
82
|
+
const key = auth[provider]?.key ?? entry.apiKey;
|
|
83
|
+
const headers = entry.headers ?? entry.models?.find((model) => model.headers)?.headers;
|
|
84
|
+
return {
|
|
85
|
+
provider,
|
|
86
|
+
source: "builtin",
|
|
87
|
+
baseUrl: entry.baseUrl ?? "",
|
|
88
|
+
api: entry.api ?? "",
|
|
89
|
+
hasKey: typeof key === "string" && key !== "",
|
|
90
|
+
keyHint: maskKey(typeof key === "string" ? key : undefined),
|
|
91
|
+
...(headers ? { headerNames: Object.keys(headers) } : {}),
|
|
92
|
+
models: (entry.models ?? []).map((model) => specView(model)),
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
// 引擎报上来的内置 provider:补上目录里没有的(例如插件注册的)
|
|
96
|
+
const known = new Set([...custom, ...fromCatalog].map((view) => view.provider));
|
|
97
|
+
const builtinViews = builtin
|
|
98
|
+
.filter((item) => !known.has(item.provider))
|
|
99
|
+
.map((item) => {
|
|
100
|
+
const key = auth[item.provider]?.key;
|
|
101
|
+
return {
|
|
102
|
+
provider: item.provider,
|
|
103
|
+
source: "builtin",
|
|
104
|
+
baseUrl: item.baseUrl ?? "",
|
|
105
|
+
api: item.api ?? "",
|
|
106
|
+
hasKey: typeof key === "string" && key !== "",
|
|
107
|
+
keyHint: maskKey(typeof key === "string" ? key : undefined),
|
|
108
|
+
models: (item.models ?? []).map((model) => specView({ id: model.id, name: model.name })),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
return [...custom, ...fromCatalog, ...builtinViews];
|
|
112
|
+
},
|
|
113
|
+
async readKey(provider) {
|
|
114
|
+
const auth = await readJson(authPath, {});
|
|
115
|
+
const key = auth[provider]?.key ?? (await readJson(catalogPath, {})).providers?.[provider]?.apiKey;
|
|
116
|
+
return typeof key === "string" && key !== "" ? key : null;
|
|
117
|
+
},
|
|
118
|
+
async removeModel(provider, modelId, seed) {
|
|
119
|
+
const models = await readJson(modelsPath, {});
|
|
120
|
+
if (!models[provider] && (await inCatalog(provider))) {
|
|
121
|
+
await writeCatalogModelPatch(options.agentDir, provider, modelId, null);
|
|
122
|
+
return { ok: true };
|
|
123
|
+
}
|
|
124
|
+
const entry = models[provider] ?? materialize(provider, seed);
|
|
125
|
+
if (!entry)
|
|
126
|
+
return { ok: false, error: `没有这个 provider:${provider}` };
|
|
127
|
+
const list = Array.isArray(entry.models) ? entry.models : [];
|
|
128
|
+
const next = list.filter((model) => model.id !== modelId);
|
|
129
|
+
if (next.length === list.length)
|
|
130
|
+
return { ok: false, error: `这个 provider 下没有模型:${modelId}` };
|
|
131
|
+
models[provider] = { ...entry, models: next, lastModified: options.now() };
|
|
132
|
+
await writeJsonPrivate(modelsPath, models);
|
|
133
|
+
return { ok: true };
|
|
134
|
+
},
|
|
135
|
+
async readHeaders(provider) {
|
|
136
|
+
const models = await readJson(modelsPath, {});
|
|
137
|
+
const own = models[provider]?.models?.find((model) => model.headers)?.headers;
|
|
138
|
+
if (own)
|
|
139
|
+
return { ...own };
|
|
140
|
+
const entry = (await readJson(catalogPath, {})).providers?.[provider];
|
|
141
|
+
const fromCatalog = entry?.headers ?? entry?.models?.find((model) => model.headers)?.headers;
|
|
142
|
+
return fromCatalog ? { ...fromCatalog } : {};
|
|
143
|
+
},
|
|
144
|
+
async get(provider, builtin = []) {
|
|
145
|
+
// 内置 provider(引擎报上来的那一批)也要能查到:否则改模型 / 停用单个模型时
|
|
146
|
+
// view 为 null,模型列表被当成空 → 停一个模型会把整个 provider 关掉
|
|
147
|
+
const all = await this.list(builtin);
|
|
148
|
+
return all.find((view) => view.provider === provider) ?? null;
|
|
149
|
+
},
|
|
150
|
+
async updateModel(provider, spec, seed) {
|
|
151
|
+
const models = await readJson(modelsPath, {});
|
|
152
|
+
// 目录里的 provider(nexita 之类):直接改 pi 的 models.json,不另建覆盖条目
|
|
153
|
+
if (!models[provider] && (await inCatalog(provider))) {
|
|
154
|
+
await writeCatalogModelPatch(options.agentDir, provider, spec.id, spec);
|
|
155
|
+
return { ok: true };
|
|
156
|
+
}
|
|
157
|
+
const entry = models[provider] ?? materialize(provider, seed);
|
|
158
|
+
if (!entry)
|
|
159
|
+
return { ok: false, error: `没有这个 provider:${provider}` };
|
|
160
|
+
const list = Array.isArray(entry.models) ? entry.models : [];
|
|
161
|
+
const index = list.findIndex((model) => model.id === spec.id);
|
|
162
|
+
if (index < 0)
|
|
163
|
+
return { ok: false, error: `这个 provider 下没有模型:${spec.id}` };
|
|
164
|
+
const current = list[index] ?? { id: spec.id };
|
|
165
|
+
const next = {
|
|
166
|
+
...current,
|
|
167
|
+
id: spec.id,
|
|
168
|
+
name: spec.name,
|
|
169
|
+
...(spec.reasoning ? { reasoning: true } : { reasoning: false }),
|
|
170
|
+
input: spec.imageInput ? ["text", "image"] : ["text"],
|
|
171
|
+
...(spec.contextWindow !== null ? { contextWindow: spec.contextWindow } : {}),
|
|
172
|
+
...(spec.maxTokens !== null ? { maxTokens: spec.maxTokens } : {}),
|
|
173
|
+
...(spec.cost ? { cost: spec.cost } : {}),
|
|
174
|
+
...(spec.costSchedule && spec.costSchedule.length > 0 ? { costSchedule: spec.costSchedule } : {}),
|
|
175
|
+
};
|
|
176
|
+
if (spec.contextWindow === null)
|
|
177
|
+
delete next.contextWindow;
|
|
178
|
+
if (spec.maxTokens === null)
|
|
179
|
+
delete next.maxTokens;
|
|
180
|
+
if (!spec.cost)
|
|
181
|
+
delete next.cost;
|
|
182
|
+
// 分时价格清空了也要真删(否则界面上删掉的时段表下次保存又回来了)
|
|
183
|
+
if (!spec.costSchedule || spec.costSchedule.length === 0)
|
|
184
|
+
delete next.costSchedule;
|
|
185
|
+
const models2 = [...list];
|
|
186
|
+
models2[index] = next;
|
|
187
|
+
models[provider] = { ...entry, models: models2, lastModified: options.now() };
|
|
188
|
+
await writeJsonPrivate(modelsPath, models);
|
|
189
|
+
return { ok: true };
|
|
190
|
+
},
|
|
191
|
+
async addModels(provider, ids) {
|
|
192
|
+
const models = await readJson(modelsPath, {});
|
|
193
|
+
if (!models[provider] && (await inCatalog(provider)))
|
|
194
|
+
return addCatalogModels(options.agentDir, provider, ids);
|
|
195
|
+
const entry = models[provider];
|
|
196
|
+
if (!entry)
|
|
197
|
+
return { ok: false, error: `没有这个 provider:${provider}` };
|
|
198
|
+
const list = Array.isArray(entry.models) ? entry.models : [];
|
|
199
|
+
const template = list.find((model) => typeof model.api === "string" && typeof model.baseUrl === "string");
|
|
200
|
+
if (!template)
|
|
201
|
+
return { ok: false, error: "这个 provider 还没有可参考的 api / baseUrl(先保存一次 provider 配置)" };
|
|
202
|
+
const existing = new Set(list.map((model) => model.id));
|
|
203
|
+
const added = [];
|
|
204
|
+
const skipped = [];
|
|
205
|
+
const nextList = [...list];
|
|
206
|
+
for (const id of ids) {
|
|
207
|
+
if (existing.has(id)) {
|
|
208
|
+
skipped.push(id);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
existing.add(id);
|
|
212
|
+
added.push(id);
|
|
213
|
+
nextList.push({
|
|
214
|
+
id,
|
|
215
|
+
name: id,
|
|
216
|
+
api: template.api,
|
|
217
|
+
baseUrl: template.baseUrl,
|
|
218
|
+
provider,
|
|
219
|
+
...(template.headers ? { headers: template.headers } : {}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (added.length === 0)
|
|
223
|
+
return { ok: true, added, skipped };
|
|
224
|
+
models[provider] = { ...entry, models: nextList, lastModified: options.now() };
|
|
225
|
+
await writeJsonPrivate(modelsPath, models);
|
|
226
|
+
return { ok: true, added, skipped };
|
|
227
|
+
},
|
|
228
|
+
async remove(provider) {
|
|
229
|
+
if (!/^[a-z0-9][a-z0-9-]{1,30}$/.test(provider))
|
|
230
|
+
return { ok: false, error: "provider 名字不合法" };
|
|
231
|
+
const models = await readJson(modelsPath, {});
|
|
232
|
+
const auth = await readJson(authPath, {});
|
|
233
|
+
let removed = false;
|
|
234
|
+
if (provider in models) {
|
|
235
|
+
delete models[provider];
|
|
236
|
+
removed = true;
|
|
237
|
+
}
|
|
238
|
+
if (removed)
|
|
239
|
+
await writeJsonPrivate(modelsPath, models);
|
|
240
|
+
if (await inCatalog(provider))
|
|
241
|
+
removed = (await removeFromCatalog(options.agentDir, provider)) || removed;
|
|
242
|
+
if (provider in auth) {
|
|
243
|
+
delete auth[provider];
|
|
244
|
+
await writeJsonPrivate(authPath, auth);
|
|
245
|
+
removed = true;
|
|
246
|
+
}
|
|
247
|
+
return { ok: true, removed };
|
|
248
|
+
},
|
|
249
|
+
async upsert(input) {
|
|
250
|
+
// 改名:把旧条目搬到新名字(否则会变成「新建一个、旧的还在」),并带上旧 key
|
|
251
|
+
if (input.previousProvider && input.previousProvider !== input.provider) {
|
|
252
|
+
const oldKey = await adoptRename(options.agentDir, input.previousProvider, input.provider);
|
|
253
|
+
if (input.apiKey === undefined && oldKey)
|
|
254
|
+
input = { ...input, apiKey: oldKey };
|
|
255
|
+
}
|
|
256
|
+
const [models, auth] = await Promise.all([
|
|
257
|
+
readJson(modelsPath, {}),
|
|
258
|
+
readJson(authPath, {}),
|
|
259
|
+
]);
|
|
260
|
+
// 新 provider 一律写 pi 的目录(models.json)—— 只有写这里,pi 的 CLI 才看得到;
|
|
261
|
+
// models-store.json 是历史遗留位置,仅对已存在的条目继续沿用
|
|
262
|
+
if (!models[input.provider]) {
|
|
263
|
+
// 新增必须能拿到 key(目录里已有 key 的允许留空不改)
|
|
264
|
+
const existingKey = auth[input.provider]?.key ?? (await readCatalog(options.agentDir)).providers?.[input.provider]?.apiKey;
|
|
265
|
+
if (input.apiKey === undefined && !existingKey) {
|
|
266
|
+
return { ok: false, error: "新增 provider 必须填 api key(否则模型调不通)" };
|
|
267
|
+
}
|
|
268
|
+
const { models: written } = await writeCatalogUpsert(options.agentDir, input);
|
|
269
|
+
const key = input.apiKey ?? existingKey;
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
view: {
|
|
273
|
+
provider: input.provider,
|
|
274
|
+
source: "builtin",
|
|
275
|
+
baseUrl: input.baseUrl,
|
|
276
|
+
api: input.api,
|
|
277
|
+
hasKey: typeof key === "string" && key !== "",
|
|
278
|
+
keyHint: maskKey(key),
|
|
279
|
+
...(input.headers ? { headerNames: Object.keys(input.headers) } : {}),
|
|
280
|
+
models: written.map((model) => specView(model)),
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const existing = models[input.provider];
|
|
285
|
+
const previous = new Map((existing?.models ?? []).map((model) => [model.id, model]));
|
|
286
|
+
const nextModels = input.models.map((model) => {
|
|
287
|
+
const base = previous.get(model.id) ?? { id: model.id };
|
|
288
|
+
return {
|
|
289
|
+
...base,
|
|
290
|
+
id: model.id,
|
|
291
|
+
name: model.name ?? base.name ?? model.id,
|
|
292
|
+
api: input.api,
|
|
293
|
+
baseUrl: input.baseUrl,
|
|
294
|
+
provider: input.provider,
|
|
295
|
+
...(input.headers ? { headers: input.headers } : base.headers ? { headers: base.headers } : {}),
|
|
296
|
+
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
// 表单里的清单是**权威**的:没勾的模型就是要去掉(之前这里会悄悄保留,
|
|
300
|
+
// 于是"取消勾选 + 保存"看起来毫无效果)
|
|
301
|
+
models[input.provider] = { ...existing, models: nextModels, lastModified: options.now() };
|
|
302
|
+
await writeJsonPrivate(modelsPath, models);
|
|
303
|
+
if (input.apiKey !== undefined) {
|
|
304
|
+
auth[input.provider] = {
|
|
305
|
+
...auth[input.provider],
|
|
306
|
+
type: auth[input.provider]?.type ?? "api_key",
|
|
307
|
+
key: input.apiKey,
|
|
308
|
+
};
|
|
309
|
+
await writeJsonPrivate(authPath, auth);
|
|
310
|
+
}
|
|
311
|
+
const key = auth[input.provider]?.key;
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
view: {
|
|
315
|
+
provider: input.provider,
|
|
316
|
+
source: "custom",
|
|
317
|
+
baseUrl: input.baseUrl,
|
|
318
|
+
api: input.api,
|
|
319
|
+
hasKey: typeof key === "string" && key !== "",
|
|
320
|
+
keyHint: maskKey(typeof key === "string" ? key : undefined),
|
|
321
|
+
...(input.headers ? { headerNames: Object.keys(input.headers) } : {}),
|
|
322
|
+
models: nextModels.map((model) => ({ id: model.id, name: model.name ?? model.id })),
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** 统一 provider 请求头:协议决定鉴权头,自定义 headers 最后覆盖同名项。 */
|
|
2
|
+
export function providerHeaders(api, apiKey, custom = {}) {
|
|
3
|
+
const headers = api === "anthropic-messages"
|
|
4
|
+
? { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
|
|
5
|
+
: { authorization: `Bearer ${apiKey}` };
|
|
6
|
+
return { ...headers, ...custom };
|
|
7
|
+
}
|