@lynn123411/dsh-a6api 1.5.8 → 1.5.10

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 CHANGED
@@ -4,7 +4,8 @@ A6API 多模型聚合站深度接入插件:将 A6API 聚合网关无缝注册
4
4
 
5
5
  ## 特性
6
6
 
7
- - **多标签页优雅布局**:重构并优化设置交互,拆分为「可用模型」、「模型目录」、「账户资产」、「基础配置」四大专属页面,分类清晰、告别页面拥挤。
7
+ - **多标签页优雅布局**:设置交互拆分为「可用模型」、「模型目录」、「账户资产」、「基础配置」四大专属页面,分类清晰、告别页面拥挤。
8
+ - **共享「API中转」设置页**:本插件的四个页面整体作为「设置 - API中转」页的首个 tab(`settings.section` id `relay`),与 `dsh-llm-agentrouter` 的端点卡片同页并列,参与者可增删。内核不允许一页被多个插件共同声明,因此每个参与者各持一份运行时逻辑一致的页壳,**先加载者当选页面宿主**、其余只注册卡片;当选者被卸载后下次启动自动改选。做法与约束见仓库 `docs/rules/shared-settings-page.md`。
8
9
  - **全真官方模型卡片复刻**:1:1 像素级复刻 A6API 官网模型卡片视图,包含品牌图标、模型名、商户 ID、渠道说明、输入/输出/缓存读写单价、实时倍率比值胶囊、实时/24h/7d 阶梯成功率点阵、延迟与缓存命中率进度条、智能标签(稳定/低价/高速/高质/保真)以及真实的固定与禁用操作。
9
10
  - **混合价估算标注(≈¥x/亿 tokens)**:每张模型卡片价格区新增绿色「≈ ¥x.xx/亿」胶囊——按卡片 24h 实测缓存命中率,把「缓存读价(命中)/ 输入价(未命中)/ 输出价」折算为每 1 亿 tokens 的混合人民币成本(输出 token 占比固定 0.35%,与官网模型市场标注口径一致),悬停展示完整估算式与每 1M 单价,对比模型贵贱一目了然;按次计费等无 token 单价的卡片不展示。
10
11
  - **给模型固定商家**:卡片一键「固定商家」/「取消固定」,直接调用 A6API 平台固定接口,把当前卡片商家绑定为该模型的服务渠道(优先路由);固定自动解析并绑定当前 API Key 令牌,异常时自动切换智能优选(平台默认兜底,无多余开关);检测到模型已固定到其他商家时,卡片自动切换为该固定商家(含价格/健康度指标),无需手动探测;卡片状态实时跟随官网——官网侧解除固定(如不接受涨价)后插件按钮自动还原。
@@ -12,15 +13,15 @@ A6API 多模型聚合站深度接入插件:将 A6API 聚合网关无缝注册
12
13
  - **路由快照时效标注**:每张模型卡片右下角标注「路由快照」时效(该卡片商户数据来自我最近一次路由请求的时刻,相对时间 + 悬停绝对时间),与全网最近成功时间叠放两行;日志中无路由记录的模型显示灰色「从未路由」。
13
14
  - **官方 vs 商户全景价格对比**:展开卡片即可查看详细渠道说明、智能标签、真实数据徽章、近 100 次样本统计,以及官方基准价与商户实时价在「输入价/输出价/缓存读/缓存写 (1M)」各维度的清晰对比表格。
14
15
  - **真实账户资产看板与调用明细**:在「账户资产」页面实时呈现真实可用余额(美元及折合人民币)、历史总消耗、累计请求次数,并实时捕获展示近期模型请求的路由明细快照(时间、模型、命中商户渠道、总耗时、Token 消耗及状态)。
15
- - **原生提供商动态注册与一键加入 DSH**:自动向 DSH LLM 运行时注入 `a6api` 目录,在「可用模型」列表点击「添加到 DSH」即可将模型自动增量同步至 `settings.yaml` DSH 对话选择器。
16
+ - **一键加入 DSH 与原生路由接入**:在「可用模型」列表点击「添加到 DSH」即可把模型增量写入 `settings.yaml` `llm-pi-ai.providers.a6api`(由 pi-ai 据此注册 `a6api` 路由),模型随即出现在 DSH 对话选择器中;插件不自行调用 LLM 运行时注册接口。
16
17
  - **节点切换与凭据一站式配置**:在「基础配置」页面支持 CDN 推荐节点(`https://api.a6api.com`)、直连备用节点(`https://a6.a6api.com`)与自定义网关节点自由切换,提供 API Key 与系统访问令牌密码显隐切换及获取教程。
17
18
  - **DSH 原生配置存储**:全部配置落位 DSH 自有配置体系——凭据(API Key / 系统访问令牌 / userId)存入 `~/.dsh/.credentials.yaml`(0600,env 优先语义),非机密状态(节点 / 模型列表)写入 `settings.yaml` 的 `llm-pi-ai.providers.a6api`,读写走 DSH 原生 `settings` / `credentials` 服务(串行写队列 + schema 校验 + 热发布),不再占用独立配置文件;旧版 `dsh-a6api-config.json` 启动时自动迁移并归档为 `.bak`。启用模型时节点随模型列表一并落盘;移除全部模型时自动移除 DSH 中的 a6api 提供商块(DSH 对手写路由无合法的零模型表示)。
18
19
  - **价格波动实时胶囊**:标题栏余额右侧常驻「价格波动:n」胶囊(白底,仅数字变色:`0` 绿/`n>0` 红/`--` 灰),基于 `GET /api/marketplace/price-notices` 的待处理条数每 60s 轻轮询(与余额同频,仅计不处置;待处理 = 通知 `relations[].state === 'open'`,即固定渠道存在未被处理/取代的价格变动,兼容显式 `pendingCount` 字段与 `status ∈ open/pending/effective` 的兜底形态),`n>0` 时一键新标签跳 `https://a6api.com/console/token`,未配置/失效或 `0` 时置灰不可点,满足“只监听不处置”的轻量诉求。
19
20
  - **「模型市场」直达胶囊**:设置页标题栏与侧边栏浮层的胶囊行中,价格波动右侧常驻同款「模型市场」胶囊(白底灰字、hover 变绿),点击新标签直达 `https://a6api.com/models`。
20
21
  - **侧边栏快捷模型卡片**:主界面左下角(设置按钮上方)常驻「A6api」入口按钮,**任何会话(含子代理)均可展开浮层**,向上弹出当前会话所用模型对应的完整商户卡片(与设置页同一数据源与操作能力:探测/固定/禁用/详情全部可用);会话切换模型时卡片实时跟随;当前会话未使用 A6api 模型时,模型卡片区域整体置灰且不可交互,顶部账户速览胶囊(余额/价格波动/模型市场)保持正常可用。
21
22
  - **侧边栏账户速览胶囊**:浮层顶部常驻「账户余额」(纯展示,每 60s 自动同步)、「价格波动」(行为与标题栏一致,可一键跳官网)与「模型市场」(点击新标签直达 `https://a6api.com/models`)三枚同款胶囊,账号级数据与模型卡片无关,加载/空态恒显示。
22
- - **后台预热无感加载**:插件随 DSH 启动即后台预热一次完整状态,之后每 60s 静默整体刷新;服务端 `/state` 将余额/模型列表/日志/固定记录等独立上游调用并行拉取(串行 5 次 RTT → 1 次)—— 侧边栏浮层与设置页打开时数据已就绪,秒开无加载等待。服务端对 `/state` 保留 120s 短缓存供轮询复用;「可用模型」页「刷新列表」按钮带 `force=1` 绕过缓存立即向 A6API 重建最新数据(v1.5.3 起)。
23
- - **模型目录管理**:「可用模型」右侧新增「模型目录」页——一键「从 A6API 获取市场模型」翻页拉取市场支持的全部模型 ID(参数初始为空),再「从 OpenRouter 一键查询」自动填充 `contextWindow / maxTokens / input`(查不到的保持留空可手动填写,每行也可单独查询;`name` 仅用户手动填写,未填则不写入 DSH);行内编辑全部采用 DSH `settings.yaml` 原生模型字段(name / contextWindow / maxTokens / input / reasoningEfforts),保存后若该模型已在 DSH 启用则立即重写 `llm-pi-ai.providers.a6api` 对应条目即时生效;OpenRouter 查询(单行「查询参数」/「一键查询」)填充到已启用模型时同样立即重写(v1.5.4 起);支持按「可用(当前令牌白名单)/ 不可用」与「参数已填 / 未填」筛选;可一键「清空目录」(二次确认)后重新从 A6API 获取并重新用 OpenRouter 填充;新获取的模型默认声明 DSH 全部思考档位(off / minimal / low / medium / high / xhigh / max)。内置静态模型目录已移除,所有参数以运行时数据为准(v1.4 起存于 `~/.dsh/dsh-a6api/catalog.json` 插件子目录,与 DSH「组件自建 home 子目录」惯例一致;旧版根目录 `dsh-a6api-catalog.json` 首次加载时自动搬迁并删除),写入 settings.yaml 时缺省字段自动省略(由 DSH 默认值兜底)。
23
+ - **后台预热无感加载**:插件随 DSH 启动即后台预热一次完整状态,之后每 60s 静默整体刷新;服务端 `/state` 将余额/模型列表/日志/固定记录等独立上游调用并行拉取(串行 5 次 RTT → 1 次)—— 侧边栏浮层与设置页打开时数据已就绪,秒开无加载等待。服务端对 `/state` 保留 120s 短缓存供轮询复用;「可用模型」页「刷新列表」按钮带 `force=1` 绕过缓存立即向 A6API 重建最新数据。
24
+ - **模型目录管理**:「可用模型」右侧的「模型目录」页——一键「从 A6API 获取市场模型」翻页拉取市场支持的全部模型 ID(参数初始为空),再「从 OpenRouter 一键查询」自动填充 `contextWindow / maxTokens / input`(查不到的保持留空可手动填写,每行也可单独查询;`name` 仅用户手动填写,未填则不写入 DSH);行内编辑全部采用 DSH `settings.yaml` 原生模型字段(name / contextWindow / maxTokens / input / reasoningEfforts),保存后若该模型已在 DSH 启用则立即重写 `llm-pi-ai.providers.a6api` 对应条目即时生效;OpenRouter 查询(单行「查询参数」/「一键查询」)填充到已启用模型时同样立即重写;支持按「可用(当前令牌白名单)/ 不可用」与「参数已填 / 未填」筛选;可一键「清空目录」(二次确认)后重新从 A6API 获取并重新用 OpenRouter 填充;新获取的模型默认声明 DSH 全部思考档位(off / minimal / low / medium / high / xhigh / max)。不内置静态模型目录,所有参数以运行时数据为准(存于 `~/.dsh/dsh-a6api/catalog.json` 插件子目录,与 DSH「组件自建 home 子目录」惯例一致;根目录遗留的 `dsh-a6api-catalog.json` 首次加载时自动搬迁并删除),写入 settings.yaml 时缺省字段自动省略(由 DSH 默认值兜底)。
24
25
 
25
26
  ## 安装
26
27
 
package/lib/client.js CHANGED
@@ -36,7 +36,7 @@ __export(index_exports, {
36
36
  name: () => name
37
37
  });
38
38
  module.exports = __toCommonJS(index_exports);
39
- var import_react7 = __toESM(require("react"), 1);
39
+ var import_react8 = __toESM(require("react"), 1);
40
40
 
41
41
  // src/client/components/A6ApiSettings.tsx
42
42
  var import_react5 = require("react");
@@ -2688,6 +2688,141 @@ var A6ApiSidebarCardBody = ({
2688
2688
  ] });
2689
2689
  };
2690
2690
 
2691
+ // src/client/relay-settings-page.js
2692
+ var import_react7 = require("react");
2693
+ var RELAY_PAGE_ID = "relay";
2694
+ var RELAY_PAGE_ORDER = 120;
2695
+ var RELAY_ITEM_SLOT = "relay.settings.item";
2696
+ var TABLIST_STYLE = {
2697
+ display: "flex",
2698
+ alignItems: "flex-end",
2699
+ gap: "22px",
2700
+ marginTop: "2px",
2701
+ marginBottom: "16px"
2702
+ };
2703
+ var TAB_STYLE = {
2704
+ appearance: "none",
2705
+ background: "transparent",
2706
+ // No border on ANY tab: the marker below is the active tab's own element, so
2707
+ // an inactive tab has nothing that could render a line.
2708
+ border: "none",
2709
+ position: "relative",
2710
+ padding: "7px 1px 11px",
2711
+ cursor: "pointer",
2712
+ font: "inherit",
2713
+ fontSize: "13px",
2714
+ lineHeight: "20px",
2715
+ color: "var(--dsw-alias-label-tertiary, inherit)"
2716
+ };
2717
+ var TAB_ACTIVE_STYLE = Object.assign({}, TAB_STYLE, {
2718
+ color: "var(--dsw-alias-label-primary, inherit)"
2719
+ });
2720
+ var TAB_MARKER_STYLE = {
2721
+ position: "absolute",
2722
+ left: 0,
2723
+ right: 0,
2724
+ bottom: 0,
2725
+ height: "2px",
2726
+ borderRadius: "2px 2px 0 0",
2727
+ background: "var(--dsw-alias-label-primary, currentColor)"
2728
+ };
2729
+ var PANEL_STYLE = { margin: 0 };
2730
+ var PANEL_HIDDEN_STYLE = { margin: 0, display: "none" };
2731
+ function readLabel(label) {
2732
+ if (typeof label === "function") return label();
2733
+ return typeof label === "string" ? label : "";
2734
+ }
2735
+ function createRelayTabs(ctx) {
2736
+ const locale = ctx.get("locale");
2737
+ let version = -1;
2738
+ let revision = -1;
2739
+ let tabs = [];
2740
+ return {
2741
+ getSnapshot: () => {
2742
+ const nextVersion = ctx.slots.getVersion(RELAY_ITEM_SLOT);
2743
+ const nextRevision = locale === void 0 ? 0 : locale.getSnapshot().revision;
2744
+ if (nextVersion === version && nextRevision === revision) return tabs;
2745
+ version = nextVersion;
2746
+ revision = nextRevision;
2747
+ tabs = ctx.slots.entries(RELAY_ITEM_SLOT).map((entry) => ({
2748
+ id: entry.options.id ?? "",
2749
+ order: entry.options.order ?? 0,
2750
+ label: readLabel(entry.options.label)
2751
+ })).sort((left, right) => left.order - right.order);
2752
+ return tabs;
2753
+ },
2754
+ subscribe: (listener) => {
2755
+ const offSlots = ctx.slots.subscribe(RELAY_ITEM_SLOT, listener);
2756
+ const offLocale = locale === void 0 ? void 0 : locale.subscribe(listener);
2757
+ return () => {
2758
+ offSlots();
2759
+ if (offLocale !== void 0) offLocale();
2760
+ };
2761
+ }
2762
+ };
2763
+ }
2764
+ function RelaySettingsSection({ renderSlot, relayTabs }) {
2765
+ const tabs = (0, import_react7.useSyncExternalStore)(
2766
+ relayTabs.subscribe,
2767
+ relayTabs.getSnapshot,
2768
+ relayTabs.getSnapshot
2769
+ );
2770
+ const [requested, setRequested] = (0, import_react7.useState)(null);
2771
+ const selected = requested !== null && tabs.some((tab) => tab.id === requested) ? requested : tabs.length > 0 ? tabs[0].id : null;
2772
+ if (selected === null) return null;
2773
+ return (0, import_react7.createElement)(
2774
+ "div",
2775
+ null,
2776
+ (0, import_react7.createElement)(
2777
+ "div",
2778
+ { role: "tablist", style: TABLIST_STYLE },
2779
+ tabs.map((tab) => (0, import_react7.createElement)(
2780
+ "button",
2781
+ {
2782
+ key: tab.id,
2783
+ type: "button",
2784
+ role: "tab",
2785
+ "aria-selected": tab.id === selected,
2786
+ style: tab.id === selected ? TAB_ACTIVE_STYLE : TAB_STYLE,
2787
+ onClick: () => {
2788
+ setRequested(tab.id);
2789
+ }
2790
+ },
2791
+ tab.label,
2792
+ tab.id === selected ? (0, import_react7.createElement)("span", { style: TAB_MARKER_STYLE, "aria-hidden": true }) : null
2793
+ ))
2794
+ ),
2795
+ tabs.map((tab) => (0, import_react7.createElement)(
2796
+ "div",
2797
+ {
2798
+ key: tab.id,
2799
+ role: "tabpanel",
2800
+ hidden: tab.id !== selected,
2801
+ style: tab.id === selected ? PANEL_STYLE : PANEL_HIDDEN_STYLE
2802
+ },
2803
+ renderSlot(RELAY_ITEM_SLOT, {}, { only: tab.id })
2804
+ ))
2805
+ );
2806
+ }
2807
+ function relayPageClaimed(ctx) {
2808
+ return ctx.slots.entries("settings.section").some((entry) => entry.options.id === RELAY_PAGE_ID);
2809
+ }
2810
+ function claimRelaySettingsPage(ctx, label) {
2811
+ if (relayPageClaimed(ctx)) return () => {
2812
+ };
2813
+ const relayTabs = createRelayTabs(ctx);
2814
+ const children = {};
2815
+ children[RELAY_ITEM_SLOT] = { kind: "list", scope: "root" };
2816
+ return ctx.slots.register({
2817
+ name: "settings.section",
2818
+ id: RELAY_PAGE_ID,
2819
+ order: RELAY_PAGE_ORDER,
2820
+ label,
2821
+ inject: () => ({ relayTabs }),
2822
+ children
2823
+ }, RelaySettingsSection);
2824
+ }
2825
+
2691
2826
  // src/client/styles/main.css
2692
2827
  var main_default = `/* A6API Plugin Styles - Clean Professional DSH Native Theme Integration */
2693
2828
 
@@ -4956,7 +5091,28 @@ body.dsh-a6api-tooltip-active [data-tooltip]::before {
4956
5091
 
4957
5092
  // src/client/index.ts
4958
5093
  var name = "@lynn123411/dsh-a6api";
4959
- var inject = ["slots"];
5094
+ var inject = ["slots", "locale"];
5095
+ var TAB_NAV = { zh: "A6api", en: "A6api" };
5096
+ var RELAY_PAGE_NAV = { zh: "API\u4E2D\u8F6C", en: "API relay" };
5097
+ var NS = "settings.a6api";
5098
+ function registerNavDicts(ctx) {
5099
+ const locale = ctx && typeof ctx.get === "function" ? ctx.get("locale") : void 0;
5100
+ if (!locale || typeof locale.register !== "function") return;
5101
+ ctx.effect(
5102
+ () => locale.register(NS, {
5103
+ zh: { pageNav: RELAY_PAGE_NAV.zh, tabNav: TAB_NAV.zh },
5104
+ en: { pageNav: RELAY_PAGE_NAV.en, tabNav: TAB_NAV.en }
5105
+ }),
5106
+ "dsh-a6api: shared page dictionaries"
5107
+ );
5108
+ }
5109
+ function navLabel(ctx, key) {
5110
+ const locale = ctx && typeof ctx.get === "function" ? ctx.get("locale") : void 0;
5111
+ if (locale && typeof locale.bind === "function") {
5112
+ return () => locale.bind(NS)(key);
5113
+ }
5114
+ return () => key === "pageNav" ? RELAY_PAGE_NAV.zh : TAB_NAV.zh;
5115
+ }
4960
5116
  function injectStyles() {
4961
5117
  if (typeof document === "undefined") return;
4962
5118
  const styleId = "dsh-a6api-styles";
@@ -5157,29 +5313,43 @@ function apply(ctx) {
5157
5313
  }
5158
5314
  if (typeof window === "undefined") return;
5159
5315
  try {
5160
- setTimeout(() => {
5161
- try {
5162
- store.warmUp();
5163
- } catch {
5164
- }
5165
- try {
5166
- store.initPricePolling();
5167
- } catch {
5168
- }
5169
- }, 1500);
5316
+ ctx.effect(() => {
5317
+ const warmupTimer = setTimeout(() => {
5318
+ try {
5319
+ store.warmUp();
5320
+ } catch {
5321
+ }
5322
+ try {
5323
+ store.initPricePolling();
5324
+ } catch {
5325
+ }
5326
+ }, 1500);
5327
+ return () => {
5328
+ clearTimeout(warmupTimer);
5329
+ try {
5330
+ store.stopAutoRefresh();
5331
+ } catch {
5332
+ }
5333
+ };
5334
+ }, "dsh-a6api: warmup & price polling");
5170
5335
  } catch {
5171
5336
  }
5172
5337
  try {
5173
5338
  const slots = ctx?.slots || (ctx?.get ? ctx.get("slots") : null);
5174
5339
  if (!slots || typeof slots.inject !== "function") return;
5175
5340
  slots.inject("settings.section", () => {
5341
+ registerNavDicts(ctx);
5342
+ return claimRelaySettingsPage(ctx, navLabel(ctx, "pageNav"));
5343
+ });
5344
+ slots.inject(RELAY_ITEM_SLOT, () => {
5176
5345
  return slots.register(
5177
5346
  {
5178
- name: "settings.section",
5347
+ name: RELAY_ITEM_SLOT,
5348
+ // id = 本插件的设置命名空间:共享页按 id 过滤本 tab 的面板,故须与卡片自身命名空间一致
5179
5349
  id: "dsh-a6api",
5180
- // 约定:自有插件设置项 order 从 110 起步进 10(原生最大 100=桌面设置),保证排在所有原生项之下
5181
- order: 120,
5182
- label: () => "A6api"
5350
+ order: 10,
5351
+ // label 由共享页投影为外层 tab 标题
5352
+ label: navLabel(ctx, "tabNav")
5183
5353
  },
5184
5354
  A6ApiSettingsPanel
5185
5355
  );
@@ -5193,7 +5363,7 @@ function apply(ctx) {
5193
5363
  order: -1,
5194
5364
  label: () => "A6api"
5195
5365
  },
5196
- (props) => import_react7.default.createElement(A6ApiSidebarCard, {
5366
+ (props) => import_react8.default.createElement(A6ApiSidebarCard, {
5197
5367
  ...props || {},
5198
5368
  getModelDirectories
5199
5369
  })