@lynn123411/dsh-llm-agentrouter 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 +4 -4
- package/cordis.patch.yml +6 -6
- package/lib/client.js +110 -110
- package/lib/index.js +42 -28
- package/package.json +7 -6
- package/LICENSE +0 -22
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# @lynn123411/dsh-llm-agentrouter
|
|
2
2
|
|
|
3
|
-
AgentRouter 中转聚合插件:把同一家中转的多个模型收拢为一条 pi-ai 路由(模型选择器只出现一个 AgentRouter 分组),国内 / 国际端点在「设置 - API中转 - AgentRouter 中转站」一键切换、下一请求即生效,外加出站请求的 User-Agent 改写与 402
|
|
3
|
+
AgentRouter 中转聚合插件:把同一家中转的多个模型收拢为一条 pi-ai 路由(模型选择器只出现一个 AgentRouter 分组),国内 / 国际端点在「设置 - API中转 - AgentRouter 中转站」一键切换、下一请求即生效,外加出站请求的 User-Agent 改写与 402 配额耗尽提示围栏。模型列表在卡片内直接编辑,写入内核「设置 - 模型」页所编辑的同一份 `llm-pi-ai` 路由数据。
|
|
4
4
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
7
|
-
- **单路由多模型**:Claude Opus 5、GPT-5.6-sol 等走同一 `agentrouter` 路由,`
|
|
8
|
-
- **模型列表可编辑**:设置卡片里直接列出这条路由的模型,`更新` 向中转站询问它现有的模型 ID(只补缺失的、不动已有条目),每个模型的 ID / 显示名 / 上下文窗口 / 最大输出 / 输入模态 / 各推理档位与线上取值都可改,保存后下一个请求即生效;新补入的模型默认 1048576 上下文、131072 输出、text+image、off..max 七档。改的就是路由自己的 `models
|
|
7
|
+
- **单路由多模型**:Claude Opus 5、GPT-5.6-sol 等走同一 `agentrouter` 路由,`reasoningEfforts` 各档位与 `compat` 开关按实测探针手写声明,模型列表不翻倍。
|
|
8
|
+
- **模型列表可编辑**:设置卡片里直接列出这条路由的模型,`更新` 向中转站询问它现有的模型 ID(只补缺失的、不动已有条目),每个模型的 ID / 显示名 / 上下文窗口 / 最大输出 / 输入模态 / 各推理档位与线上取值都可改,保存后下一个请求即生效;新补入的模型默认 1048576 上下文、131072 输出、text+image、off..max 七档。改的就是路由自己的 `models`——`llm-pi-ai` 这条 profile 插件配置里的 `providers.agentrouter.models`,内核「设置 - 模型」页对该路由注册的写入地址也正是这里,两边同一份数据;patch 里手写的那几条是「重置」的基线。
|
|
9
9
|
- **端点一键切换**:设置卡片两个大选项(国内端点 / 国际端点,附带真实 host 展示),点击即写即生效,无需重启;API Key 只存 `$DSH_HOME/.credentials.yaml` 的 `AGENTROUTER_API_KEY` 引用。
|
|
10
10
|
- **共享「API中转」设置页**:本插件的端点卡片与 `dsh-a6api` 的面板挂在同一个设置页里(`settings.section` id `relay` 的页内两个 tab,参与者可增删)。内核不允许一页被多个插件共同声明,因此每个参与者各持一份运行时逻辑一致的页壳,**先加载者当选页面宿主**、其余只注册卡片;当选者被卸载后下次启动自动改选。做法与约束见仓库 `docs/rules/shared-settings-page.md`。
|
|
11
11
|
- **请求围栏**:重写 relay 要求的 `User-Agent`(适配器强制署名下沉到 `fetch` 层替换),哨兵 host(`.internal` 不可解析)兜底防裸奔,402 配额耗尽的 JSON 错误体改写为可读提示。
|
|
12
|
-
-
|
|
12
|
+
- **内核设置接入**:本插件 `Config` 里需要实时生效的字段都声明为 `.volatile()`,设置面据此派生表单、并把配置写回同一批引用(`ctx.inject(['settings'])` + `ctx.settings.configure({ auto: false })` 记下「本插件自带 UI」);客户端卡片挂在共享设置页的子 slot,读写的是 `llm-agentrouter` 这条 profile 插件配置,条目 id 即设置命名空间。
|
|
13
13
|
|
|
14
14
|
## 安装
|
|
15
15
|
|
package/cordis.patch.yml
CHANGED
|
@@ -56,15 +56,15 @@
|
|
|
56
56
|
# name and an adapter-supplied description a hand-declared pi-ai route has no
|
|
57
57
|
# field for. The group title is therefore the only reachable surface — and it is
|
|
58
58
|
# a name, not a notice, so it stays a name. The endpoint is explained where it is
|
|
59
|
-
# changed: 设置 →
|
|
59
|
+
# changed: 设置 → API中转 → AgentRouter 中转站.
|
|
60
60
|
#
|
|
61
61
|
# WHOLE-OBJECT OVERWRITE, NOT A DEEP MERGE — RE-CHECK ON EVERY dsh-base BUMP
|
|
62
62
|
# The loader applies a patch entry as `target[key] = value`
|
|
63
|
-
# (applyEntryPatches,
|
|
64
|
-
# llm-pi-ai row's whole config object: nothing a default config
|
|
65
|
-
# at any depth. That is safe today only because dsh-base
|
|
66
|
-
# `- id: llm-pi-ai` with a `name:` and no `config:` at all
|
|
67
|
-
# (dsh-base/cordis.patch.yml:
|
|
63
|
+
# (applyEntryPatches, @deepseek-ai/dsh-app-boot), so the `config:` below
|
|
64
|
+
# REPLACES the llm-pi-ai row's whole config object: nothing a default config
|
|
65
|
+
# carried survives, at any depth. That is safe today only because dsh-base
|
|
66
|
+
# mounts `- id: llm-pi-ai` with a `name:` and no `config:` at all
|
|
67
|
+
# (dsh-base/cordis.patch.yml:127-128), so there is nothing to lose. If a future
|
|
68
68
|
# dsh-base gives that row a default config, this patch swallows it SILENTLY —
|
|
69
69
|
# diff the shipped llm-pi-ai row against this file on every dsh-base bump and
|
|
70
70
|
# fold whatever it adds into the `providers:` map below.
|
package/lib/client.js
CHANGED
|
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let jsx = require("react/jsx-runtime");
|
|
8
8
|
let react = require("react");
|
|
9
|
+
const { Button, Checkbox, Input } = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
10
|
|
|
10
11
|
//#region styles
|
|
11
12
|
/*
|
|
@@ -16,44 +17,40 @@ window.__ModuleLoader__.load({
|
|
|
16
17
|
*/
|
|
17
18
|
const CSS = [
|
|
18
19
|
".dshAr_card{display:flex;flex-direction:column;gap:10px}",
|
|
19
|
-
".dshAr_lead{margin:0;color:var(--dsw-alias-label-tertiary);font
|
|
20
|
+
".dshAr_lead{margin:0;color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-xs-13)}",
|
|
20
21
|
".dshAr_choices{display:flex;gap:10px;margin:0;padding:0;border:0;flex-wrap:wrap}",
|
|
21
|
-
".dshAr_choice{flex:1 1 200px;min-width:0;display:flex;gap:9px;align-items:flex-start;padding:11px 13px;border:
|
|
22
|
+
".dshAr_choice{flex:1 1 200px;min-width:0;display:flex;gap:9px;align-items:flex-start;padding:11px 13px;border:0.5px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3);cursor:pointer;transition:background-color var(--ds-transition-duration) var(--ds-ease-in-out),border-color var(--ds-transition-duration) var(--ds-ease-in-out)}",
|
|
22
23
|
".dshAr_choice:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
23
24
|
".dshAr_choice[data-selected=true]{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 1px var(--dsw-alias-state-business-primary) inset}",
|
|
24
25
|
".dshAr_choice[data-disabled=true]{cursor:default;opacity:.55}",
|
|
25
|
-
".dshAr_choice input{margin:3px 0 0}",
|
|
26
|
+
".dshAr_choice input{margin:3px 0 0;accent-color:var(--dsw-alias-brand-primary)}",
|
|
27
|
+
".dshAr_choice input:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}",
|
|
26
28
|
".dshAr_choiceText{display:flex;flex-direction:column;gap:2px;min-width:0}",
|
|
27
|
-
".dshAr_choiceTitle{color:var(--dsw-alias-label-primary);font-
|
|
28
|
-
".dshAr_choiceHost{overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary);font-family:var(--ds-font-family-code)
|
|
29
|
-
".dshAr_choiceHint{color:var(--dsw-alias-label-tertiary);font
|
|
30
|
-
".dshAr_status{margin:0;min-height:18px;color:var(--dsw-alias-label-tertiary);font
|
|
29
|
+
".dshAr_choiceTitle{color:var(--dsw-alias-label-primary);font:var(--dsw-font-s-strong-14)}",
|
|
30
|
+
".dshAr_choiceHost{overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-xxs-12);font-family:var(--ds-font-family-code)}",
|
|
31
|
+
".dshAr_choiceHint{color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-xxs-12)}",
|
|
32
|
+
".dshAr_status{margin:0;min-height:18px;color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-xxs-12)}",
|
|
31
33
|
".dshAr_status[data-kind=error]{color:var(--dsw-alias-state-error-primary)}",
|
|
32
|
-
".dshAr_models{display:flex;flex-direction:column;gap:8px;margin-top:4px;padding-top:12px;border-top:
|
|
34
|
+
".dshAr_models{display:flex;flex-direction:column;gap:8px;margin-top:4px;padding-top:12px;border-top:0.5px solid var(--dsw-alias-border-l2)}",
|
|
33
35
|
".dshAr_modelsHead{display:flex;align-items:center;gap:10px;flex-wrap:wrap}",
|
|
34
|
-
".dshAr_modelsHead h4{margin:0;color:var(--dsw-alias-label-primary);font-
|
|
36
|
+
".dshAr_modelsHead h4{margin:0;color:var(--dsw-alias-label-primary);font:var(--dsw-font-s-strong-14)}",
|
|
35
37
|
".dshAr_actions{display:flex;gap:8px;flex-wrap:wrap}",
|
|
36
38
|
".dshAr_modelsHead .dshAr_actions{margin-left:auto}",
|
|
37
|
-
".dshAr_button{appearance:none;padding:5px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}",
|
|
38
|
-
".dshAr_button:hover:enabled{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
39
|
-
".dshAr_button:disabled{cursor:default;opacity:.5}",
|
|
40
39
|
".dshAr_modelsList{display:flex;flex-direction:column;gap:6px;margin:0;padding:0;list-style:none}",
|
|
41
|
-
".dshAr_model{margin:0;padding:8px 10px;border:
|
|
42
|
-
".dshAr_modelHead{display:flex;gap:8px;align-items:baseline;cursor:pointer;color:var(--dsw-alias-label-primary);font
|
|
40
|
+
".dshAr_model{margin:0;padding:8px 10px;border:0.5px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3)}",
|
|
41
|
+
".dshAr_modelHead{display:flex;gap:8px;align-items:baseline;cursor:pointer;color:var(--dsw-alias-label-primary);font:var(--dsw-font-xs-13)}",
|
|
43
42
|
".dshAr_modelName{font-weight:600}",
|
|
44
|
-
".dshAr_modelId{overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary);font-family:var(--ds-font-family-code)
|
|
43
|
+
".dshAr_modelId{overflow-wrap:anywhere;color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-xxxs-11);font-family:var(--ds-font-family-code)}",
|
|
45
44
|
".dshAr_fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin-top:10px}",
|
|
46
45
|
".dshAr_field{display:flex;flex-direction:column;gap:3px;min-width:0}",
|
|
47
|
-
".dshAr_fieldLabel{color:var(--dsw-alias-label-secondary);font
|
|
48
|
-
".dshAr_input{min-width:0;padding:4px 7px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px}",
|
|
49
|
-
".dshAr_input:disabled{opacity:.55}",
|
|
46
|
+
".dshAr_fieldLabel{color:var(--dsw-alias-label-secondary);font:var(--dsw-font-xxxs-11)}",
|
|
50
47
|
".dshAr_inputs{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:10px}",
|
|
51
|
-
".dshAr_toggle{display:flex;gap:5px;align-items:center;color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px}",
|
|
52
48
|
".dshAr_efforts{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:6px;margin-top:10px}",
|
|
53
|
-
".dshAr_effort{display:flex;gap:6px;align-items:center;
|
|
54
|
-
".dshAr_effortLevel{min-width:
|
|
55
|
-
".dshAr_effortWire{flex:1 1 60px}",
|
|
49
|
+
".dshAr_effort{display:flex;gap:6px;align-items:center;min-width:0}",
|
|
50
|
+
".dshAr_effortLevel{min-width:76px}",
|
|
51
|
+
".dshAr_effortWire{flex:1 1 60px;min-width:0}",
|
|
56
52
|
".dshAr_modelActions{display:flex;justify-content:flex-end;margin-top:10px}",
|
|
53
|
+
"@media (prefers-reduced-motion: reduce){.dshAr_choice{transition:none}}",
|
|
57
54
|
].join("");
|
|
58
55
|
const CSS_TAG_ID = "dsh-llm-agentrouter/EndpointCard.css";
|
|
59
56
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(CSS_TAG_ID) + "]") === null) {
|
|
@@ -108,6 +105,7 @@ window.__ModuleLoader__.load({
|
|
|
108
105
|
modalityText: "文本",
|
|
109
106
|
modalityImage: "图片",
|
|
110
107
|
wireNone: "留空 = 不发送",
|
|
108
|
+
wireLabel: "「%s」档位发送给中转站的取值",
|
|
111
109
|
levelOff: "off",
|
|
112
110
|
levelMinimal: "minimal",
|
|
113
111
|
levelLow: "low",
|
|
@@ -164,6 +162,7 @@ window.__ModuleLoader__.load({
|
|
|
164
162
|
modalityText: "Text",
|
|
165
163
|
modalityImage: "Image",
|
|
166
164
|
wireNone: "empty = send nothing",
|
|
165
|
+
wireLabel: "Wire value sent for the %s level",
|
|
167
166
|
levelOff: "off",
|
|
168
167
|
levelMinimal: "minimal",
|
|
169
168
|
levelLow: "low",
|
|
@@ -186,9 +185,9 @@ window.__ModuleLoader__.load({
|
|
|
186
185
|
const FIELD = "endpoint";
|
|
187
186
|
|
|
188
187
|
/**
|
|
189
|
-
* Read the endpoint from a resolved
|
|
190
|
-
* schema documents when the
|
|
191
|
-
* @param {unknown} value - the
|
|
188
|
+
* Read the endpoint from a resolved entry, falling back to the default the
|
|
189
|
+
* schema documents when the entry is not readable yet.
|
|
190
|
+
* @param {unknown} value - the form snapshot's resolved value.
|
|
192
191
|
* @returns {string} an endpoint key.
|
|
193
192
|
*/
|
|
194
193
|
function endpointOf(value) {
|
|
@@ -199,7 +198,7 @@ window.__ModuleLoader__.load({
|
|
|
199
198
|
/**
|
|
200
199
|
* Read the host table from a resolved section so each choice can show the
|
|
201
200
|
* origin it actually means.
|
|
202
|
-
* @param {unknown} value - the
|
|
201
|
+
* @param {unknown} value - the form snapshot's resolved value.
|
|
203
202
|
* @returns {Record<string, string>} host per endpoint key.
|
|
204
203
|
*/
|
|
205
204
|
function hostsOf(value) {
|
|
@@ -208,23 +207,23 @@ window.__ModuleLoader__.load({
|
|
|
208
207
|
}
|
|
209
208
|
|
|
210
209
|
/**
|
|
211
|
-
* The relay card: the endpoint radio group over this plugin's own
|
|
212
|
-
*
|
|
210
|
+
* The relay card: the endpoint radio group over this plugin's own profile
|
|
211
|
+
* entry config, above the model list the route in `llm-pi-ai` actually serves.
|
|
213
212
|
*
|
|
214
213
|
* A choice writes immediately rather than staging behind a Save button. The
|
|
215
214
|
* namespace has exactly one user-facing field and the write is reversible in
|
|
216
215
|
* one click, so a staged form would add a step without protecting anything —
|
|
217
|
-
* and `
|
|
218
|
-
* two halves are independent: each reads and writes its own
|
|
219
|
-
* deployment that exposes only one of them still renders the other.
|
|
216
|
+
* and the form's own `set` already fences the write with the revision it read.
|
|
217
|
+
* The two halves are independent: each reads and writes its own entry's form,
|
|
218
|
+
* so a deployment that exposes only one of them still renders the other.
|
|
220
219
|
*
|
|
221
|
-
* @param {object} props - the injected
|
|
220
|
+
* @param {object} props - the injected config forms plus the bound translator.
|
|
222
221
|
* @returns {JSX.Element} the card.
|
|
223
222
|
*/
|
|
224
|
-
function EndpointCard({
|
|
223
|
+
function EndpointCard({ form, t, routeForm, operations }) {
|
|
225
224
|
const snapshot = react.useSyncExternalStore(
|
|
226
|
-
react.useCallback((listener) =>
|
|
227
|
-
() =>
|
|
225
|
+
react.useCallback((listener) => form.subscribe(listener), [form]),
|
|
226
|
+
() => form.getSnapshot(),
|
|
228
227
|
);
|
|
229
228
|
const [pending, setPending] = react.useState(null);
|
|
230
229
|
const [failed, setFailed] = react.useState(false);
|
|
@@ -239,10 +238,14 @@ window.__ModuleLoader__.load({
|
|
|
239
238
|
setPending(endpoint);
|
|
240
239
|
setFailed(false);
|
|
241
240
|
Promise.resolve()
|
|
242
|
-
.then(() =>
|
|
241
|
+
.then(() => form.set(FIELD, endpoint))
|
|
243
242
|
.then(
|
|
244
|
-
|
|
243
|
+
// The form answers whether the Host accepted the write and runs its own
|
|
244
|
+
// recovery read; a refusal and a transport rejection are one outcome here:
|
|
245
|
+
// the choice did not land, so the card says so and leaves the section alone.
|
|
246
|
+
(accepted) => {
|
|
245
247
|
setPending(null);
|
|
248
|
+
if (!accepted) setFailed(true);
|
|
246
249
|
},
|
|
247
250
|
() => {
|
|
248
251
|
setPending(null);
|
|
@@ -314,7 +317,7 @@ window.__ModuleLoader__.load({
|
|
|
314
317
|
role: shown.kind === "error" ? "alert" : "status",
|
|
315
318
|
children: shown.text,
|
|
316
319
|
}),
|
|
317
|
-
|
|
320
|
+
routeForm === undefined ? null : jsx.jsx(ModelList, { routeForm, operations, t }),
|
|
318
321
|
],
|
|
319
322
|
});
|
|
320
323
|
}
|
|
@@ -322,12 +325,14 @@ window.__ModuleLoader__.load({
|
|
|
322
325
|
|
|
323
326
|
//#region model list
|
|
324
327
|
/**
|
|
325
|
-
* The
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
328
|
+
* The profile entry id the relay route is declared in — `llm-pi-ai`'s own
|
|
329
|
+
* patch row, which the adapter also registers as the settings namespace of
|
|
330
|
+
* its configurable providers (`settingsPath: ["providers", route]`). This
|
|
331
|
+
* plugin's model list is NOT its own preference: it is the `agentrouter`
|
|
332
|
+
* route's `models` array, the same entry and the same path the kernel's
|
|
333
|
+
* Models page edits, which is why editing it is what changes the model
|
|
334
|
+
* picker. Spelled rather than imported, for the same reason as SETTINGS_NS in
|
|
335
|
+
* the plugin region below: a browser bundle must not depend on a Host package.
|
|
331
336
|
*/
|
|
332
337
|
const PI_AI_SETTINGS_NS = "llm-pi-ai";
|
|
333
338
|
/** The route key inside that namespace this plugin owns. */
|
|
@@ -560,8 +565,7 @@ window.__ModuleLoader__.load({
|
|
|
560
565
|
className: "dshAr_field",
|
|
561
566
|
children: [
|
|
562
567
|
jsx.jsx("span", { className: "dshAr_fieldLabel", children: t(label) }),
|
|
563
|
-
jsx.jsx(
|
|
564
|
-
className: "dshAr_input",
|
|
568
|
+
jsx.jsx(Input, {
|
|
565
569
|
type: "text",
|
|
566
570
|
value: row[name],
|
|
567
571
|
disabled: readOnly,
|
|
@@ -605,25 +609,21 @@ window.__ModuleLoader__.load({
|
|
|
605
609
|
className: "dshAr_inputs",
|
|
606
610
|
children: [
|
|
607
611
|
jsx.jsx("span", { className: "dshAr_fieldLabel", children: t("fieldInput") }),
|
|
608
|
-
...MODALITIES.map((modality) => jsx.
|
|
609
|
-
"
|
|
612
|
+
...MODALITIES.map((modality) => jsx.jsx(
|
|
613
|
+
"span",
|
|
610
614
|
{
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
? row.input.filter((entry) => entry !== modality)
|
|
622
|
-
: [...row.input, modality],
|
|
623
|
-
}),
|
|
615
|
+
"data-modality": modality,
|
|
616
|
+
"data-model": String(index),
|
|
617
|
+
children: jsx.jsx(Checkbox, {
|
|
618
|
+
checked: row.input.includes(modality),
|
|
619
|
+
disabled: readOnly,
|
|
620
|
+
label: t(MODALITY_LABELS[modality]),
|
|
621
|
+
onChange: (next) => onPatch(index, {
|
|
622
|
+
input: next
|
|
623
|
+
? [...row.input, modality]
|
|
624
|
+
: row.input.filter((entry) => entry !== modality),
|
|
624
625
|
}),
|
|
625
|
-
|
|
626
|
-
],
|
|
626
|
+
}),
|
|
627
627
|
},
|
|
628
628
|
modality,
|
|
629
629
|
)),
|
|
@@ -635,30 +635,28 @@ window.__ModuleLoader__.load({
|
|
|
635
635
|
children: EFFORT_LEVELS.map((level) => {
|
|
636
636
|
const entry = row.efforts.find((candidate) => candidate.level === level)
|
|
637
637
|
?? { level, on: false, wire: "" };
|
|
638
|
+
const levelKey = "level" + level.charAt(0).toUpperCase() + level.slice(1);
|
|
638
639
|
return jsx.jsxs(
|
|
639
|
-
"
|
|
640
|
+
"div",
|
|
640
641
|
{
|
|
641
642
|
className: "dshAr_effort",
|
|
642
643
|
"data-level": level,
|
|
644
|
+
"data-model": String(index),
|
|
643
645
|
children: [
|
|
644
|
-
jsx.jsx(
|
|
645
|
-
type: "checkbox",
|
|
646
|
+
jsx.jsx(Checkbox, {
|
|
646
647
|
checked: entry.on,
|
|
647
648
|
disabled: readOnly,
|
|
648
|
-
"data-model": String(index),
|
|
649
|
-
"data-effort": level,
|
|
650
|
-
onChange: () => onEffort(index, level, { on: !entry.on }),
|
|
651
|
-
}),
|
|
652
|
-
jsx.jsx("span", {
|
|
653
649
|
className: "dshAr_effortLevel",
|
|
654
|
-
|
|
650
|
+
label: t(levelKey),
|
|
651
|
+
onChange: (next) => onEffort(index, level, { on: next }),
|
|
655
652
|
}),
|
|
656
|
-
jsx.jsx(
|
|
657
|
-
className: "
|
|
653
|
+
jsx.jsx(Input, {
|
|
654
|
+
className: "dshAr_effortWire",
|
|
658
655
|
type: "text",
|
|
659
656
|
value: entry.wire,
|
|
660
657
|
placeholder: level === "off" ? t("wireNone") : level,
|
|
661
658
|
disabled: readOnly || !entry.on,
|
|
659
|
+
"aria-label": fill(t("wireLabel"), [t(levelKey)]),
|
|
662
660
|
"data-model": String(index),
|
|
663
661
|
"data-wire": level,
|
|
664
662
|
onChange: (event) => onEffort(index, level, { wire: event.target.value }),
|
|
@@ -671,9 +669,9 @@ window.__ModuleLoader__.load({
|
|
|
671
669
|
}),
|
|
672
670
|
jsx.jsx("div", {
|
|
673
671
|
className: "dshAr_modelActions",
|
|
674
|
-
children: jsx.jsx(
|
|
675
|
-
|
|
676
|
-
|
|
672
|
+
children: jsx.jsx(Button, {
|
|
673
|
+
variant: "ghost",
|
|
674
|
+
size: "sm",
|
|
677
675
|
"data-action": "remove",
|
|
678
676
|
disabled: readOnly,
|
|
679
677
|
onClick: () => onRemove(index),
|
|
@@ -689,8 +687,8 @@ window.__ModuleLoader__.load({
|
|
|
689
687
|
}
|
|
690
688
|
|
|
691
689
|
/**
|
|
692
|
-
* Store faces for a caller that injects no Host operations —
|
|
693
|
-
* or a
|
|
690
|
+
* Store faces for a caller that injects no Host operations — a test rendering
|
|
691
|
+
* this component directly, or a registration whose optional Remote is absent.
|
|
694
692
|
*/
|
|
695
693
|
const NO_OPERATIONS_STORE = { subscribe: () => () => {}, snapshot: () => 0 };
|
|
696
694
|
|
|
@@ -699,24 +697,25 @@ window.__ModuleLoader__.load({
|
|
|
699
697
|
* serves, the 更新 action that asks the relay what it currently offers, and
|
|
700
698
|
* the write that puts the result back on the route.
|
|
701
699
|
*
|
|
702
|
-
* Reads come from the shared
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
700
|
+
* Reads come from the shared config form of the `llm-pi-ai` profile entry, so
|
|
701
|
+
* the card and the kernel's Models page can never disagree about the route.
|
|
702
|
+
* Writes go through the Remote call the operations were built with, NOT
|
|
703
|
+
* through the form's `mutate`: that path answers only whether the Host
|
|
704
|
+
* accepted, while this card has to show the Host's own diagnostic — a stale
|
|
705
|
+
* revision, or a payload the adapter's schema refused. The kernel's own
|
|
706
|
+
* Models page writes the same namespace the same way.
|
|
708
707
|
*
|
|
709
708
|
* An edit lives in this component until 保存; 重置 drops the draft and, when
|
|
710
709
|
* the user layer owns the list, removes that override so the composition's
|
|
711
710
|
* hand-declared list serves again.
|
|
712
711
|
*
|
|
713
|
-
* @param {object} props - the route
|
|
712
|
+
* @param {object} props - the route form, the Host operations, and `t`.
|
|
714
713
|
* @returns {JSX.Element} the model list.
|
|
715
714
|
*/
|
|
716
|
-
function ModelList({
|
|
715
|
+
function ModelList({ routeForm, operations, t }) {
|
|
717
716
|
const snapshot = react.useSyncExternalStore(
|
|
718
|
-
react.useCallback((listener) =>
|
|
719
|
-
() =>
|
|
717
|
+
react.useCallback((listener) => routeForm.subscribe(listener), [routeForm]),
|
|
718
|
+
() => routeForm.getSnapshot(),
|
|
720
719
|
);
|
|
721
720
|
const ops = operations ?? {};
|
|
722
721
|
// The optional Remote faces mount after this plugin activates, so the
|
|
@@ -869,9 +868,9 @@ window.__ModuleLoader__.load({
|
|
|
869
868
|
const shown = status === null
|
|
870
869
|
? { kind: "info", text: "" }
|
|
871
870
|
: { kind: status.kind, text: fill(t(status.key), status.values) };
|
|
872
|
-
const action = (name, label, disabled, onClick) => jsx.jsx(
|
|
873
|
-
|
|
874
|
-
|
|
871
|
+
const action = (name, label, disabled, onClick) => jsx.jsx(Button, {
|
|
872
|
+
variant: name === "save" ? "primary" : "outline",
|
|
873
|
+
size: "sm",
|
|
875
874
|
"data-action": name,
|
|
876
875
|
disabled,
|
|
877
876
|
onClick,
|
|
@@ -985,13 +984,11 @@ window.__ModuleLoader__.load({
|
|
|
985
984
|
position: "relative",
|
|
986
985
|
padding: "7px 1px 11px",
|
|
987
986
|
cursor: "pointer",
|
|
988
|
-
font: "
|
|
989
|
-
|
|
990
|
-
lineHeight: "20px",
|
|
991
|
-
color: "var(--dsw-alias-label-tertiary, inherit)",
|
|
987
|
+
font: "var(--dsw-font-xs-13)",
|
|
988
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
992
989
|
};
|
|
993
990
|
const TAB_ACTIVE_STYLE = Object.assign({}, TAB_STYLE, {
|
|
994
|
-
color: "var(--dsw-alias-label-primary
|
|
991
|
+
color: "var(--dsw-alias-label-primary)",
|
|
995
992
|
});
|
|
996
993
|
/** The kernel's own tab marker: a 2px rounded bar under the active label. */
|
|
997
994
|
const TAB_MARKER_STYLE = {
|
|
@@ -1001,7 +998,7 @@ window.__ModuleLoader__.load({
|
|
|
1001
998
|
bottom: 0,
|
|
1002
999
|
height: "2px",
|
|
1003
1000
|
borderRadius: "2px 2px 0 0",
|
|
1004
|
-
background: "var(--dsw-alias-label-primary
|
|
1001
|
+
background: "var(--dsw-alias-label-primary)",
|
|
1005
1002
|
};
|
|
1006
1003
|
/**
|
|
1007
1004
|
* Panels stay mounted (hidden) so each card keeps its local state. `display:
|
|
@@ -1144,27 +1141,30 @@ window.__ModuleLoader__.load({
|
|
|
1144
1141
|
/** Dictionary namespace owned by this plugin. */
|
|
1145
1142
|
const NS = "settings.agentrouter";
|
|
1146
1143
|
/**
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1149
|
-
*
|
|
1144
|
+
* This plugin's profile entry id — its settings namespace, and the key its
|
|
1145
|
+
* config form is addressed by. Spelled rather than imported: a browser bundle
|
|
1146
|
+
* must not depend on a Host package, so both halves state the same literal
|
|
1147
|
+
* (the Host's is `AGENTROUTER_SETTINGS_NAMESPACE`).
|
|
1150
1148
|
*/
|
|
1151
1149
|
const SETTINGS_NS = "llm-agentrouter";
|
|
1152
1150
|
/** Services this plugin needs from the browser runtime. */
|
|
1153
|
-
const inject = ["slots", "locale", "
|
|
1151
|
+
const inject = ["slots", "locale", "configForms"];
|
|
1154
1152
|
|
|
1155
1153
|
/**
|
|
1156
1154
|
* Register the endpoint card inside the shared 「API中转」 page, which this
|
|
1157
|
-
* plugin carries the shell for and claims when it activates first
|
|
1155
|
+
* plugin carries the shell for and claims when it activates first, and bind
|
|
1156
|
+
* the two config forms it reads (this plugin's own entry, and the adapter's
|
|
1157
|
+
* route) out of the shared configuration forms service.
|
|
1158
1158
|
* @param {object} ctx - the browser plugin context.
|
|
1159
1159
|
*/
|
|
1160
1160
|
function apply(ctx) {
|
|
1161
1161
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "llm-agentrouter: dictionaries");
|
|
1162
1162
|
const t = ctx.locale.bind(NS);
|
|
1163
|
-
const
|
|
1164
|
-
// The model list is not this plugin's own
|
|
1165
|
-
// route inside the adapter's
|
|
1166
|
-
//
|
|
1167
|
-
const
|
|
1163
|
+
const form = ctx.configForms.get(SETTINGS_NS);
|
|
1164
|
+
// The model list is not this plugin's own entry: it is the `agentrouter`
|
|
1165
|
+
// route inside the adapter's entry, the same document the kernel's Models
|
|
1166
|
+
// page edits, so the card derives from that entry's shared config form.
|
|
1167
|
+
const routeForm = ctx.configForms.get(PI_AI_SETTINGS_NS);
|
|
1168
1168
|
// Both Host faces are OPTIONAL reads: a deployment whose Client assembly
|
|
1169
1169
|
// mounts neither still shows the endpoint switch, and the model list
|
|
1170
1170
|
// degrades to read-only with the reason rendered instead of going blank.
|
|
@@ -1230,14 +1230,14 @@ window.__ModuleLoader__.load({
|
|
|
1230
1230
|
ctx.slots.register(
|
|
1231
1231
|
{
|
|
1232
1232
|
name: RELAY_ITEM_SLOT,
|
|
1233
|
-
// id =
|
|
1233
|
+
// id = this plugin's profile entry id; the shared page filters its
|
|
1234
1234
|
// panels by this id, so it must be the plugin's own key.
|
|
1235
1235
|
id: SETTINGS_NS,
|
|
1236
1236
|
order: 20,
|
|
1237
1237
|
// Rendered as the card's tab title inside the shared page.
|
|
1238
1238
|
label: () => t("title"),
|
|
1239
1239
|
locale: NS,
|
|
1240
|
-
inject: () => ({
|
|
1240
|
+
inject: () => ({ form, t, routeForm, operations }),
|
|
1241
1241
|
},
|
|
1242
1242
|
EndpointCard,
|
|
1243
1243
|
),
|
package/lib/index.js
CHANGED
|
@@ -38,10 +38,11 @@ import z from '@deepseek-ai/schemastery'
|
|
|
38
38
|
const name = 'llm-agentrouter'
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* This plugin's profile entry id, which is also its settings namespace and the
|
|
42
|
+
* license key its configuration form is addressed by.
|
|
42
43
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
44
|
+
* The id is declared by the bundle patch (`cordis.patch.yml`), and the browser
|
|
45
|
+
* half addresses the same literal without importing this module.
|
|
45
46
|
*/
|
|
46
47
|
const AGENTROUTER_SETTINGS_NAMESPACE = 'llm-agentrouter'
|
|
47
48
|
|
|
@@ -53,6 +54,7 @@ const Config = z.object({
|
|
|
53
54
|
endpoint: z
|
|
54
55
|
.union([z.const('cn'), z.const('intl')])
|
|
55
56
|
.default('cn')
|
|
57
|
+
.volatile()
|
|
56
58
|
.description('relay endpoint requests are sent to: cn (domestic) or intl (international)'),
|
|
57
59
|
/**
|
|
58
60
|
* Host per endpoint key. Configuration rather than a constant so a moved
|
|
@@ -61,6 +63,7 @@ const Config = z.object({
|
|
|
61
63
|
endpoints: z
|
|
62
64
|
.dict(z.string())
|
|
63
65
|
.default({ cn: 'ps.air-outer.com', intl: 'agentrouter.org' })
|
|
66
|
+
.volatile()
|
|
64
67
|
.description('host for each endpoint key'),
|
|
65
68
|
/**
|
|
66
69
|
* The host the route's `baseURL` names. Requests to it are rewritten to the
|
|
@@ -70,6 +73,7 @@ const Config = z.object({
|
|
|
70
73
|
sentinel: z
|
|
71
74
|
.string()
|
|
72
75
|
.default('relay.agentrouter.internal')
|
|
76
|
+
.volatile()
|
|
73
77
|
.description('placeholder host in the route baseURL that the fence replaces with the selected endpoint'),
|
|
74
78
|
/**
|
|
75
79
|
* The exact User-Agent the relay accepts. It is the whole authentication of
|
|
@@ -79,6 +83,7 @@ const Config = z.object({
|
|
|
79
83
|
userAgent: z
|
|
80
84
|
.string()
|
|
81
85
|
.default('claude-cli/2.1.161 (external, cli)')
|
|
86
|
+
.volatile()
|
|
82
87
|
.description('User-Agent value sent to the relay in place of the harness attribution'),
|
|
83
88
|
/** Report the installed fence once on activation. */
|
|
84
89
|
announce: z.boolean().default(true),
|
|
@@ -91,9 +96,19 @@ const Config = z.object({
|
|
|
91
96
|
quotaHint: z
|
|
92
97
|
.string()
|
|
93
98
|
.default('Claude / GPT 本批额度已用完,请等待下一批投放。')
|
|
99
|
+
.volatile()
|
|
94
100
|
.description('text appended to relay 402 quota errors'),
|
|
95
101
|
})
|
|
96
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Every field the fence reads carries `.volatile()` and is therefore a live
|
|
105
|
+
* reference rather than a snapshot: the settings plane writes the reference in
|
|
106
|
+
* place, the next request reads the new value, and no reload intervenes. That
|
|
107
|
+
* is also what makes these fields the ones the schema-derived config form
|
|
108
|
+
* shows and accepts writes to (`ctx.settings.describe`/`mutate`). `announce`
|
|
109
|
+
* is deliberately plain: it is read once, on activation.
|
|
110
|
+
*/
|
|
111
|
+
|
|
97
112
|
/**
|
|
98
113
|
* Resolve a `fetch` argument to its URL without consuming a request body.
|
|
99
114
|
* @param {unknown} input - the first `fetch` argument.
|
|
@@ -122,12 +137,13 @@ function urlOf(input) {
|
|
|
122
137
|
*/
|
|
123
138
|
function routingTable(config) {
|
|
124
139
|
const table = new Map()
|
|
125
|
-
const
|
|
126
|
-
const
|
|
140
|
+
const endpoints = config.endpoints.get()
|
|
141
|
+
const selected = endpoints[config.endpoint.get()]
|
|
142
|
+
const sentinel = config.sentinel.get().trim().toLowerCase()
|
|
127
143
|
if (sentinel.length > 0 && typeof selected === 'string' && selected.trim().length > 0) {
|
|
128
144
|
table.set(sentinel, selected.trim())
|
|
129
145
|
}
|
|
130
|
-
for (const host of Object.values(
|
|
146
|
+
for (const host of Object.values(endpoints)) {
|
|
131
147
|
if (typeof host !== 'string') continue
|
|
132
148
|
const trimmed = host.trim()
|
|
133
149
|
if (trimmed.length > 0) table.set(trimmed.toLowerCase(), trimmed)
|
|
@@ -151,21 +167,22 @@ function routingTable(config) {
|
|
|
151
167
|
* rebuilds it as `application/json`. Every other response is left untouched.
|
|
152
168
|
*
|
|
153
169
|
* @param {typeof fetch} native - the fetch this wrapper delegates to.
|
|
154
|
-
* @param {
|
|
170
|
+
* @param {ReturnType<typeof Config>} config - the entry configuration; every
|
|
171
|
+
* field read here is a `.volatile()` reference, so the value is the one the
|
|
172
|
+
* settings plane last wrote, not the one activation resolved.
|
|
155
173
|
* @returns {typeof fetch} the wrapping fetch.
|
|
156
174
|
*/
|
|
157
|
-
function fenceFetch(native,
|
|
175
|
+
function fenceFetch(native, config) {
|
|
158
176
|
return function agentRouterFetch(input, init) {
|
|
159
177
|
const url = urlOf(input)
|
|
160
178
|
if (url === undefined) return native(input, init)
|
|
161
179
|
|
|
162
|
-
const config = current()
|
|
163
180
|
const destination = routingTable(config).get(url.host.toLowerCase())
|
|
164
181
|
if (destination === undefined) return native(input, init)
|
|
165
182
|
|
|
166
183
|
const isRequest = typeof Request === 'function' && input instanceof Request
|
|
167
184
|
const headers = new Headers(init?.headers ?? (isRequest ? input.headers : undefined))
|
|
168
|
-
headers.set('user-agent', config.userAgent)
|
|
185
|
+
headers.set('user-agent', config.userAgent.get())
|
|
169
186
|
|
|
170
187
|
// Same host means the sentinel was not involved: rewrite the header only,
|
|
171
188
|
// and leave the caller's own URL object or Request identity alone.
|
|
@@ -185,7 +202,8 @@ function fenceFetch(native, current) {
|
|
|
185
202
|
// as an event stream, which provider SDKs otherwise surface as an opaque
|
|
186
203
|
// transport failure. Annotate those with the configured hint; every other
|
|
187
204
|
// response passes through untouched.
|
|
188
|
-
|
|
205
|
+
const hint = config.quotaHint.get()
|
|
206
|
+
return hint === '' ? pending : annotateQuotaError(pending, hint)
|
|
189
207
|
}
|
|
190
208
|
}
|
|
191
209
|
|
|
@@ -257,22 +275,18 @@ function requestInitOf(request) {
|
|
|
257
275
|
}
|
|
258
276
|
|
|
259
277
|
/**
|
|
260
|
-
* Install the relay fence and
|
|
278
|
+
* Install the relay fence and declare how this plugin's configuration is edited.
|
|
261
279
|
* @param {import('@deepseek-ai/cordis').Context} ctx - the plugin's context.
|
|
262
280
|
* @param {ReturnType<typeof Config>} config - resolved entry configuration.
|
|
263
281
|
*/
|
|
264
282
|
function apply(ctx, config) {
|
|
265
|
-
// The
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
current = source
|
|
273
|
-
},
|
|
274
|
-
onChange: () => {},
|
|
275
|
-
})
|
|
283
|
+
// The settings plane derives this plugin's form from the Config schema above
|
|
284
|
+
// and writes it back into the same `.volatile()` references the fence reads.
|
|
285
|
+
// `auto: false` because the endpoint card owns this plugin's UI (lib/client.js);
|
|
286
|
+
// the registration is optional, so a deployment with no settings service at all
|
|
287
|
+
// (headless, or before the service mounts) keeps the composed entry as-is.
|
|
288
|
+
ctx.inject(['settings'], (child) => {
|
|
289
|
+
child.effect(() => child.settings.configure({ auto: false }, ctx.fiber))
|
|
276
290
|
})
|
|
277
291
|
|
|
278
292
|
ctx.effect(() => {
|
|
@@ -281,7 +295,7 @@ function apply(ctx, config) {
|
|
|
281
295
|
ctx.logger.warn('llm-agentrouter: no global fetch to fence; relay requests will be unroutable and rejected')
|
|
282
296
|
return () => {}
|
|
283
297
|
}
|
|
284
|
-
const fenced = fenceFetch(previous,
|
|
298
|
+
const fenced = fenceFetch(previous, config)
|
|
285
299
|
globalThis.fetch = fenced
|
|
286
300
|
return () => {
|
|
287
301
|
// Restore only what this plugin installed: a later wrapper layered on top
|
|
@@ -293,10 +307,10 @@ function apply(ctx, config) {
|
|
|
293
307
|
if (config.announce) {
|
|
294
308
|
const table = routingTable(config)
|
|
295
309
|
ctx.logger.info(
|
|
296
|
-
'llm-agentrouter: endpoint %
|
|
297
|
-
config.endpoint,
|
|
298
|
-
table.get(config.sentinel.trim().toLowerCase()) ?? 'unrouted',
|
|
299
|
-
config.userAgent,
|
|
310
|
+
'llm-agentrouter: endpoint %s (%s), sending %s',
|
|
311
|
+
config.endpoint.get(),
|
|
312
|
+
table.get(config.sentinel.get().trim().toLowerCase()) ?? 'unrouted',
|
|
313
|
+
config.userAgent.get(),
|
|
300
314
|
)
|
|
301
315
|
}
|
|
302
316
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lynn123411/dsh-llm-agentrouter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AgentRouter 中转聚合:一条 pi-ai 路由承载多模型(设置卡片内可从中转站拉取、逐个编辑模型与参数)+ 国内/国际端点一键切换 + 出站请求 User-Agent/402
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "AgentRouter 中转聚合:一条 pi-ai 路由承载多模型(设置卡片内可从中转站拉取、逐个编辑模型与参数)+ 国内/国际端点一键切换 + 出站请求 User-Agent/402-配额改写围栏。模型列表写入内核「设置 - 模型」页所编辑的同一份 llm-pi-ai 路由数据。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"client": {
|
|
34
34
|
"inject": [
|
|
35
35
|
"@deepseek-ai/dsh-client-locale",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
36
37
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
37
38
|
],
|
|
38
39
|
"platform": "web"
|
|
@@ -41,7 +42,6 @@
|
|
|
41
42
|
"files": [
|
|
42
43
|
"lib/",
|
|
43
44
|
"cordis.patch.yml",
|
|
44
|
-
"LICENSE",
|
|
45
45
|
"README.md"
|
|
46
46
|
],
|
|
47
47
|
"publishConfig": {
|
|
@@ -51,13 +51,14 @@
|
|
|
51
51
|
"test": "node --test test/*.test.mjs"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
|
-
"node": ">=22"
|
|
54
|
+
"node": ">=22",
|
|
55
|
+
"dsh": "0.1.7-rc.2"
|
|
55
56
|
},
|
|
56
57
|
"dependencies": {
|
|
57
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
58
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
58
59
|
},
|
|
59
60
|
"peerDependencies": {
|
|
60
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
61
|
+
"@deepseek-ai/cordis": "^4.0.4"
|
|
61
62
|
},
|
|
62
63
|
"peerDependenciesMeta": {
|
|
63
64
|
"@deepseek-ai/cordis": {
|
package/LICENSE
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 aqiu817
|
|
4
|
-
Copyright (c) 2026 lynn123411 (alpha.5 compatibility fork: settings/slots API migration, package rename)
|
|
5
|
-
|
|
6
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
-
in the Software without restriction, including without limitation the rights
|
|
9
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
-
furnished to do so, subject to the following conditions:
|
|
12
|
-
|
|
13
|
-
The above copyright notice and this permission notice shall be included in all
|
|
14
|
-
copies or substantial portions of the Software.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
-
SOFTWARE.
|