@luchenglong/cli-bridge 0.0.0-stage → 0.2.2
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/CHANGELOG.md +148 -0
- package/LICENSE +21 -0
- package/MCP-POLICY.md +53 -0
- package/PROVIDER-CONTRACT.md +235 -0
- package/README.en.md +98 -0
- package/README.md +150 -2
- package/assets/indicator-states.svg +62 -0
- package/bin/agy-quota.mjs +223 -0
- package/core/bridge-kit.mjs +549 -0
- package/core/scheduling.mjs +149 -0
- package/docs/ARCHITECTURE.md +117 -0
- package/docs/FALLBACK-AND-INDICATOR.md +93 -0
- package/docs/INSTALL.md +118 -0
- package/docs/MCP-POLICY.zh.md +49 -0
- package/docs/MIGRATION.md +43 -0
- package/docs/RELEASE-SOP.md +77 -0
- package/docs/history/agy/CHANGELOG.md +326 -0
- package/docs/history/codebuddy/CHANGELOG.md +668 -0
- package/docs/history/mimo/CHANGELOG.md +346 -0
- package/dynamic/agy.host.js +1237 -0
- package/dynamic/agy.host.template.mjs +140 -0
- package/dynamic/codebuddy.host.js +1787 -0
- package/dynamic/codebuddy.host.template.mjs +162 -0
- package/dynamic/mimo.host.js +1732 -0
- package/dynamic/mimo.host.template.mjs +169 -0
- package/home-plugin/cli-bridge-indicator/cordis.patch.yml +31 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +606 -0
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +420 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +137 -0
- package/home-plugin/cli-bridge-indicator/lib/settings.mjs +300 -0
- package/mcp/agy-mcp-server.mjs +6 -0
- package/mcp/cli-bridge-mcp.mjs +184 -0
- package/mcp/codebuddy-mcp-server.mjs +6 -0
- package/mcp/mimo-mcp-server.mjs +6 -0
- package/mcp/providers/agy.mjs +254 -0
- package/mcp/providers/codebuddy.mjs +397 -0
- package/mcp/providers/mimo.mjs +290 -0
- package/package.json +87 -4
- package/preset/agy-first/agent.cordis.yml +141 -0
- package/preset/agy-first/cli-bridge.mjs +169 -0
- package/preset/agy-first/plugin-list.mjs +26 -0
- package/preset/agy-first/preset.yml +3 -0
- package/preset/codebuddy-first/agent.cordis.yml +141 -0
- package/preset/codebuddy-first/cli-bridge.mjs +188 -0
- package/preset/codebuddy-first/plugin-list.mjs +20 -0
- package/preset/codebuddy-first/preset.yml +3 -0
- package/preset/mimo-first/agent.cordis.yml +143 -0
- package/preset/mimo-first/cli-bridge.mjs +236 -0
- package/preset/mimo-first/plugin-list.mjs +39 -0
- package/preset/mimo-first/preset.yml +3 -0
- package/preset/standard-base.mjs +102 -0
- package/providers/agy.mjs +398 -0
- package/providers/codebuddy.mjs +926 -0
- package/providers/mimo.mjs +864 -0
- package/providers/registry.mjs +34 -0
- package/scripts/build.mjs +235 -0
- package/scripts/deploy.mjs +61 -0
- package/scripts/verify.mjs +148 -0
package/README.md
CHANGED
|
@@ -1,3 +1,151 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cli-bridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**简体中文** · [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
> 一个用于 **DeepSeek Harness (DSH)** 的 Cordis 插件包:把编码/构建/调试/排查等实际工作
|
|
6
|
+
> **优先派发给本机 CLI agent** —— Google **Antigravity(`agy`)**、腾讯 **CodeBuddy Code**、
|
|
7
|
+
> 小米 **MiMo(MiMoCode CLI / MiMo Desktop)** —— DSH 全程掌控这些 CLI(非交互、无提示),
|
|
8
|
+
> 并在 **限流 / 网络不通** 时弹窗让用户选择是否回退到 **DSH 本地 API 配置**;会话标题栏提供
|
|
9
|
+
> **一盏聚合实时状态灯**(三家分区),设置面板统一三家偏好与凭据。
|
|
10
|
+
>
|
|
11
|
+
> 本包整合并取代三个旧包:`agy-first-bridge`(1.7.1 终版)、`codebuddy-first-bridge`(1.6.2 终版)、
|
|
12
|
+
> `mimo-first-bridge`(1.2.9 终版)。旧包已 deprecated,功能全部并入此处;迁移见
|
|
13
|
+
> [docs/MIGRATION.md](docs/MIGRATION.md)。
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 这是什么
|
|
20
|
+
|
|
21
|
+
`@luchenglong/cli-bridge` 给运行中的 DSH 注入:
|
|
22
|
+
|
|
23
|
+
1. **每家三个模型工具** —— `<id>_run` / `<id>_continue` / `<id>_status`
|
|
24
|
+
(agy 另有 `agy_quota`,mimo 另有 `mimo_settings`),把任务转交给本机 CLI 执行;
|
|
25
|
+
**工具名与入参语义与旧包逐字一致**,历史会话提示词零改动继续可用。
|
|
26
|
+
2. **一段 CLI 优先策略提示**(每家各自 POLICY 段),让模型在所有模式下优先把实际工作
|
|
27
|
+
派发给对应 CLI,原生工具只用于只读查询与最终验证。
|
|
28
|
+
3. **一盏聚合状态灯(家级插件,随 DSH 启动加载)**:标题栏 pill 汇总三家状态
|
|
29
|
+
(绿=就绪/成功、黄=运行中、红=失败、灰=不可用、琥珀=本地回退、preset 高亮=本会话运行在桥接预设下);
|
|
30
|
+
悬停看当前步骤,点击展开按 provider、按项目(工作目录)的活动面板 ——
|
|
31
|
+
每项目实时步骤折叠(当前工具 + 参数摘要 + 轨迹)、runs/tokens 累计、最近完成运行。
|
|
32
|
+
4. **统一设置面板**(DSH 设置页「CLI Bridge」段):**横向标签页** —— 「总设置」+ 每个 CLI 一页,
|
|
33
|
+
点名字切换、同屏只渲染一张卡(页长不随 CLI 数量增长);每家字段由 provider 自己声明
|
|
34
|
+
(meta 驱动渲染);token 类字段永不明文回传;保存写
|
|
35
|
+
`{DSH_HOME}/cli-bridge-settings.json`(按 provider 分区);**旧包设置文件自动一次性迁移**。
|
|
36
|
+
5. **调度(0.2.0+)**:「总设置」页勾选**启用哪些 CLI**(决定调用面数量)+ `↑↓` 排定**调用优先级**,
|
|
37
|
+
一次保存同时贯通预设、动态宿主与 MCP 三种形态 —— 详见 [调度](#调度启用哪些-cli与调用优先级)。
|
|
38
|
+
|
|
39
|
+
## 四种形态
|
|
40
|
+
|
|
41
|
+
同一套 provider 描述符(单一事实来源)驱动四种落地形态:
|
|
42
|
+
|
|
43
|
+
| 形态 | 位置 | 能力 | 状态灯 |
|
|
44
|
+
| --- | --- | --- | --- |
|
|
45
|
+
| **持久 Agent Preset**(推荐) | [`preset/<dir>/`](preset/) | 工具 + POLICY + 回退弹窗 + 三家预设(`cordis-agy` / `codebuddy-first` / `cordis-mimo`) | ✅(家级灯自动点亮) |
|
|
46
|
+
| **家级插件(指示灯+设置+声明注册)** | [`home-plugin/cli-bridge-indicator/`](home-plugin/cli-bridge-indicator/) | HTTP 路由 + 浏览器 pill/面板/设置 + **preset 声明制注册** | 本体 |
|
|
47
|
+
| **动态宿主(沙箱插件)** | [`dynamic/<id>.host.js`](dynamic/)(由模板生成) | 无 preset 时在任意组合里挂载工具 | ✅(经收集器汇入) |
|
|
48
|
+
| **MCP stdio server** | [`mcp/cli-bridge-mcp.mjs`](mcp/)(bin `cli-bridge-mcp`) | 任何 MCP 客户端(Claude Code/Cursor/DSH 外部)直连本机三家 CLI | ✅(快照文件桥接,见下) |
|
|
49
|
+
|
|
50
|
+
MCP 接入(一个 server 暴露全部三家工具;只想要一家用兼容入口或 `--provider=`):
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"cli-bridge": {
|
|
56
|
+
"command": "node",
|
|
57
|
+
"args": ["/path/to/node_modules/@luchenglong/cli-bridge/mcp/cli-bridge-mcp.mjs"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
旧包单家配置迁移:`node mcp/agy-mcp-server.mjs`(等价 `--provider=agy`),env 变量名不变。
|
|
64
|
+
|
|
65
|
+
**MCP 形态的状态灯**:MCP 是独立进程,没有 `ctx.emit`,故它把每台状态原子写入
|
|
66
|
+
`{DSH_HOME}/cli-bridge-mcp-<id>.json`(90s 内有效),家级灯轮询合并 —— 你在 DSH 外面
|
|
67
|
+
用 Claude Code 跑 agy,DSH 标题栏照样亮。
|
|
68
|
+
|
|
69
|
+
## 调度(启用哪些 CLI 与调用优先级)
|
|
70
|
+
|
|
71
|
+
设置页「CLI Bridge 桥接」→「总设置」标签:勾选要启用的 CLI(决定智能体可调用的 CLI 数量),
|
|
72
|
+
`↑↓` 排定调用优先级(首位最高)。每个 CLI 另有自己的标签页(后端字段驱动 + 诊断)。
|
|
73
|
+
|
|
74
|
+
**落盘**:`{DSH_HOME}/cli-bridge-settings.json` 顶层 `_scheduling = { enabled, priority }`
|
|
75
|
+
(下划线前缀 = 非 provider 分区,旧版本读该文件时整区忽略 → 纯增量兼容)。
|
|
76
|
+
派生 `order = priority ∩ enabled`:`enabled` 是集合(数组顺序无意义),`priority` 的**数组顺序即优先级**。
|
|
77
|
+
|
|
78
|
+
**一次配置贯穿四种形态**:
|
|
79
|
+
|
|
80
|
+
| 生效面 | 行为 |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| preset / 动态宿主 | `apply()` 首行门禁:禁用面**不注册任何工具、策略段、心跳**;启用面按优先级排注册顺序 |
|
|
83
|
+
| 声明制 preset 注册 | 三个 preset 恒常注册(禁用 ≠ 从目录消失,仍可切入排查),注册顺序按优先级 |
|
|
84
|
+
| MCP 形态 | `enabled` 过滤工具面、`order` 排序;启动读一次(重启子进程刷新),家级 `bin\` 服务器**每请求现读**(面板改完立即生效) |
|
|
85
|
+
| 状态灯与设置视图 | `GET /cli-bridge/status`、`GET /cli-bridge/settings` 每次轮询现读并附 `scheduling` 快照(`enabled/priority/order/all/labels`) |
|
|
86
|
+
|
|
87
|
+
**语义与边界**:
|
|
88
|
+
|
|
89
|
+
- **fail-open**:文件缺失 / 坏值 / 无 fs → 全部启用(读不到配置 ≠ 关掉所有桥);
|
|
90
|
+
只有**显式** `enabled: []` 才是「全关」,且面板会红字提示「三形态都不会注册任何 CLI 工具」。
|
|
91
|
+
- **钉住优先**:env `CLI_BRIDGE_PROVIDER` 或 `--provider=<id>`(MCP 形态的薄封装)优先级最高,
|
|
92
|
+
用于「这个客户端只要 agy」的单面用法;宿主形态传 `ignorePin`,不会被全局 env 反噬。
|
|
93
|
+
- **指示灯不长明**:超过 10 分钟无活动且未运行的项目不再渲染为常亮灯(历史仍保留在点击弹窗里)。
|
|
94
|
+
|
|
95
|
+
**加第 N 家 CLI**:只改 `providers/registry.mjs`(id 唯一轴)+ 交付该 provider 的描述符;
|
|
96
|
+
调度卡的选项、标签页、状态灯、设置保存循环与构建派生物全部自动跟上 —— 契约见
|
|
97
|
+
[PROVIDER-CONTRACT.md](PROVIDER-CONTRACT.md) §3.1。
|
|
98
|
+
|
|
99
|
+
## 架构速览
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
providers/registry.mjs ── createProvider(id) ─┬─ providers/agy.mjs ┐
|
|
103
|
+
core/bridge-kit.mjs(状态引擎/runner/限流/ ├─ providers/codebuddy.mjs ├─ 同一描述符驱动
|
|
104
|
+
回退/渲染/MCP 载荷,零 import) └─ providers/mimo.mjs ┘ 四种形态
|
|
105
|
+
home-plugin/cli-bridge-indicator/
|
|
106
|
+
├ index.mjs 统一状态机(租约/合并/stale) + HTTP 路由 + 收集器
|
|
107
|
+
├ settings.mjs 分区设置读写 + 掩码 + 旧文件迁移
|
|
108
|
+
├ preset-declarations.mjs 声明制注册(Form-A 自注册,全版本可用)
|
|
109
|
+
└ client.js 浏览器 pill/面板/设置(标签页 + 调度卡 + 陈旧过滤;注册 id ≡ 包名,红线,CI 锁)
|
|
110
|
+
preset/standard-base.mjs + preset/<dir>/plugin-list.mjs ── scripts/build.mjs ──> agent.cordis.yml / preset.yml / dynamic/*.host.js
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- 开发/移植第三家 CLI?契约见 [PROVIDER-CONTRACT.md](PROVIDER-CONTRACT.md),一次实现一个描述符即可接入全部形态。
|
|
114
|
+
- 为什么 preset id 和工具名不变?旧会话头/提示词引用它们(红线清单在契约 §0)。
|
|
115
|
+
- 生成物为什么提交进仓库?fresh clone 直接可装可用;`node scripts/build.mjs --check` 在 CI/prepack 防漂移。
|
|
116
|
+
|
|
117
|
+
## 安装
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install -g @luchenglong/cli-bridge # MCP 用:cli-bridge-mcp 已在 PATH
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
DSH 接线(home-plugin bundle + presets + 指示灯一次装齐)见 **[docs/INSTALL.md](docs/INSTALL.md)**。
|
|
124
|
+
要点:包带 `dsh.bundle.patch`(bundle 面自注册),DSH profile 加入该 bundle 后
|
|
125
|
+
三盏能力 + 三家 preset + 设置面板 + 状态灯一次到位;本仓库脚本 `npm run deploy:local`
|
|
126
|
+
只做到「暂存目录」,不改运行时接线(需先停 DSH)。
|
|
127
|
+
|
|
128
|
+
## 开发
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
node scripts/build.mjs # 生成 dynamic 宿主 + legacy YAML
|
|
132
|
+
node scripts/verify.mjs # 发布红线(client id/exports/描述符完整度…)
|
|
133
|
+
node --test # 单测(含每家 tests/<id>/)
|
|
134
|
+
node mcp/cli-bridge-mcp.mjs --check # MCP 自检
|
|
135
|
+
npm run check # 三闸门全量
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 文档
|
|
139
|
+
|
|
140
|
+
- [docs/INSTALL.md](docs/INSTALL.md) — 安装与接线(含旧包迁移)
|
|
141
|
+
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — 三形态共享内核设计、状态机与红线论证
|
|
142
|
+
- [docs/MIGRATION.md](docs/MIGRATION.md) — 从三个旧包迁移对照表
|
|
143
|
+
- [MCP-POLICY.md](MCP-POLICY.md) / [中文版](docs/MCP-POLICY.zh.md) — 给外部 MCP 客户端(Claude Code/Codex)的派发策略卡
|
|
144
|
+
- [docs/RELEASE-SOP.md](docs/RELEASE-SOP.md) — 发版流程与红线检查
|
|
145
|
+
- [docs/FALLBACK-AND-INDICATOR.md](docs/FALLBACK-AND-INDICATOR.md) — 回退弹窗与状态灯语义详解
|
|
146
|
+
- [PROVIDER-CONTRACT.md](PROVIDER-CONTRACT.md) — provider 接入契约(冻结)
|
|
147
|
+
- [CHANGELOG.md](CHANGELOG.md) + [docs/history/](docs/history/) — 统一历史与三家 lineage 全量历史
|
|
148
|
+
|
|
149
|
+
## 许可
|
|
150
|
+
|
|
151
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="330" viewBox="0 0 760 330" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">
|
|
2
|
+
<rect width="760" height="330" fill="#ffffff"/>
|
|
3
|
+
<text x="20" y="30" font-size="17" font-weight="600" fill="#1f2328">CLI Bridge 聚合状态灯(一盏 pill,三家分区)</text>
|
|
4
|
+
|
|
5
|
+
<!-- helper row labels -->
|
|
6
|
+
<g font-size="12.5" fill="#57606a">
|
|
7
|
+
<text x="20" y="70">就绪 / 成功</text>
|
|
8
|
+
<text x="20" y="115">运行中</text>
|
|
9
|
+
<text x="20" y="160">失败</text>
|
|
10
|
+
<text x="20" y="205">不可用(未安装/未登录)</text>
|
|
11
|
+
<text x="20" y="250">本地回退(DSH API 接管中)</text>
|
|
12
|
+
<text x="20" y="295">预设高亮(本会话跑在桥接 preset 下)</text>
|
|
13
|
+
</g>
|
|
14
|
+
|
|
15
|
+
<!-- row 1 ready -->
|
|
16
|
+
<g>
|
|
17
|
+
<rect x="230" y="52" rx="12" width="240" height="26" fill="#dafbe1" stroke="#1a7f37"/>
|
|
18
|
+
<circle cx="246" cy="65" r="6" fill="#1a7f37"/>
|
|
19
|
+
<text x="258" y="69" font-size="12.5" fill="#116326">AGY · CB · MIMO 就绪</text>
|
|
20
|
+
</g>
|
|
21
|
+
<!-- row 2 running -->
|
|
22
|
+
<g>
|
|
23
|
+
<rect x="230" y="97" rx="12" width="252" height="26" fill="#dbeafe" stroke="#3b82f6"/>
|
|
24
|
+
<circle cx="246" cy="110" r="6" fill="#3b82f6">
|
|
25
|
+
<animate attributeName="opacity" values="1;0.35;1" dur="1.2s" repeatCount="indefinite"/>
|
|
26
|
+
</circle>
|
|
27
|
+
<text x="258" y="114" font-size="12.5" fill="#1d4ed8">AGY 工作中… read(index.mjs)</text>
|
|
28
|
+
</g>
|
|
29
|
+
<!-- row 3 failed -->
|
|
30
|
+
<g>
|
|
31
|
+
<rect x="230" y="142" rx="12" width="240" height="26" fill="#ffebe9" stroke="#cf222e"/>
|
|
32
|
+
<circle cx="246" cy="155" r="6" fill="#cf222e"/>
|
|
33
|
+
<text x="258" y="159" font-size="12.5" fill="#a40e26">CB 失败 HUNG_TIMEOUT</text>
|
|
34
|
+
</g>
|
|
35
|
+
<!-- row 4 unavailable -->
|
|
36
|
+
<g>
|
|
37
|
+
<rect x="230" y="187" rx="12" width="240" height="26" fill="#f6f8fa" stroke="#afb8c1"/>
|
|
38
|
+
<circle cx="246" cy="200" r="6" fill="#afb8c1"/>
|
|
39
|
+
<text x="258" y="204" font-size="12.5" fill="#57606a">MIMO 未安装(灰)</text>
|
|
40
|
+
</g>
|
|
41
|
+
<!-- row 5 fallback -->
|
|
42
|
+
<g>
|
|
43
|
+
<rect x="230" y="232" rx="12" width="262" height="26" fill="#fff1e5" stroke="#bc4c00"/>
|
|
44
|
+
<circle cx="246" cy="245" r="6" fill="#bc4c00"/>
|
|
45
|
+
<text x="258" y="249" font-size="12.5" fill="#8a3700">AGY 限流 → 本地 API 回退中</text>
|
|
46
|
+
</g>
|
|
47
|
+
<!-- row 6 preset highlight -->
|
|
48
|
+
<g>
|
|
49
|
+
<rect x="230" y="277" rx="12" width="300" height="26" fill="#dafbe1" stroke="#1a7f37"/>
|
|
50
|
+
<circle cx="246" cy="290" r="6" fill="#1a7f37"/>
|
|
51
|
+
<text x="258" y="294" font-size="12.5" fill="#116326">CB · MIMO 就绪 · 本会话 CB 优先(preset 租约)</text>
|
|
52
|
+
</g>
|
|
53
|
+
|
|
54
|
+
<text x="560" y="70" font-size="12" fill="#57606a">悬停:该项目当前步骤</text>
|
|
55
|
+
<text x="560" y="92" font-size="12" fill="#57606a">点击:按 provider · 按项目</text>
|
|
56
|
+
<text x="560" y="110" font-size="12" fill="#57606a">展开活动面板(trail/runs/tokens)</text>
|
|
57
|
+
<text x="560" y="150" font-size="12" fill="#57606a">数据源三路合并:</text>
|
|
58
|
+
<text x="560" y="168" font-size="12" fill="#57606a">preset 事件 · dynamic 收集器</text>
|
|
59
|
+
<text x="560" y="186" font-size="12" fill="#57606a">· MCP 快照文件(90s 过期)</text>
|
|
60
|
+
<text x="560" y="226" font-size="12" fill="#57606a">preset 租约 75s TTL / 30s 心跳,</text>
|
|
61
|
+
<text x="560" y="244" font-size="12" fill="#57606a">关会话灯自动灭</text>
|
|
62
|
+
</svg>
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/agy-quota.mjs
|
|
3
|
+
//
|
|
4
|
+
// 查询 agy 所用 Google AI 套餐(Antigravity OAuth consumer)的池子额度。
|
|
5
|
+
// 链路(参考开源项目 lbjlaq/Antigravity-Manager,已实测打通):
|
|
6
|
+
// 1. 读 Windows 凭据管理器 gemini:antigravity(agy 登录时写入的 OAuth token)
|
|
7
|
+
// 2. 用 refresh_token + 公开的 Antigravity OAuth client 刷新 access_token
|
|
8
|
+
// 3. POST fetchAvailableModels -> 每模型池子剩余百分比 remainingFraction
|
|
9
|
+
// 4. POST retrieveUserQuotaSummary -> 分组套餐余量(weekly + 5h 双窗口)
|
|
10
|
+
//
|
|
11
|
+
// 用法:
|
|
12
|
+
// node bin/agy-quota.mjs # stdout 输出 JSON
|
|
13
|
+
// node bin/agy-quota.mjs --summary # 仅输出一行紧凑摘要(灯 tooltip 用)
|
|
14
|
+
//
|
|
15
|
+
// 输出 JSON:
|
|
16
|
+
// { ok, models: [{name, percentage, resetTime, displayName}...],
|
|
17
|
+
// groups: [{displayName, buckets: [{bucketId, window, remainingFraction, resetTime}]}],
|
|
18
|
+
// tier, tokenSource, error? }
|
|
19
|
+
//
|
|
20
|
+
// 独立运行,无 npm 依赖(Node 18+ fetch + child_process)。
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from 'node:child_process'
|
|
23
|
+
import { createRequire } from 'node:module'
|
|
24
|
+
const require = createRequire(import.meta.url)
|
|
25
|
+
|
|
26
|
+
const CLOUDCODE_ENDPOINTS = [
|
|
27
|
+
'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels',
|
|
28
|
+
'https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels',
|
|
29
|
+
'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels',
|
|
30
|
+
]
|
|
31
|
+
const SUMMARY_ENDPOINTS = [
|
|
32
|
+
'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:retrieveUserQuotaSummary',
|
|
33
|
+
'https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary',
|
|
34
|
+
'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary',
|
|
35
|
+
]
|
|
36
|
+
const TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
|
37
|
+
// Antigravity 官方 OAuth client(consumer 登录用)。为避免在仓库中硬编码凭据
|
|
38
|
+
// (GitHub secret-scanning 会拦截),运行时优先读环境变量,否则从本机已安装的
|
|
39
|
+
// Antigravity 扩展文件动态提取(jlcodes.antigravity-cockpit 扩展的 out/extension.js
|
|
40
|
+
// 中硬编码了官方 client_id / client_secret)。
|
|
41
|
+
const OAUTH_CLIENT_ID = process.env.AGY_OAUTH_CLIENT_ID || detectAntigravityOAuth()[0]
|
|
42
|
+
const OAUTH_CLIENT_SECRET = process.env.AGY_OAUTH_CLIENT_SECRET || detectAntigravityOAuth()[1]
|
|
43
|
+
|
|
44
|
+
function detectAntigravityOAuth() {
|
|
45
|
+
try {
|
|
46
|
+
const fs = require('node:fs')
|
|
47
|
+
const { homedir } = require('node:os')
|
|
48
|
+
const { join } = require('node:path')
|
|
49
|
+
const exts = join(homedir(), '.antigravity', 'extensions')
|
|
50
|
+
const entries = fs.readdirSync(exts, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.includes('antigravity-cockpit'))
|
|
51
|
+
for (const dir of entries) {
|
|
52
|
+
const f = join(exts, dir.name, 'out', 'extension.js')
|
|
53
|
+
if (!fs.existsSync(f)) continue
|
|
54
|
+
const txt = fs.readFileSync(f, 'utf8')
|
|
55
|
+
const idm = txt.match(/["']([0-9]{12,}-[a-z0-9]{20,}\.apps\.googleusercontent\.com)["']/)
|
|
56
|
+
const sm = txt.match(/["'](GOCSPX-[A-Za-z0-9_-]{20,})["']/)
|
|
57
|
+
if (idm && sm) return [idm[1], sm[1]]
|
|
58
|
+
}
|
|
59
|
+
} catch (e) { /* ignore */ }
|
|
60
|
+
return ['', '']
|
|
61
|
+
}
|
|
62
|
+
const USER_AGENT = 'vscode/1.95.3 (Antigravity/4.3.0)'
|
|
63
|
+
const CRED_TARGET = 'gemini:antigravity'
|
|
64
|
+
|
|
65
|
+
// 读 Windows 凭据管理器条目(CredRead P/Invoke),返回 blob 的 UTF-8 文本。
|
|
66
|
+
// 方案:用 csc.exe 现编译一个最小 C# 程序(避免 PowerShell Add-Type 在
|
|
67
|
+
// 本机因 LIB 自引用失效的问题;编译只需几秒,成功后在 %TEMP% 缓存)。
|
|
68
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
69
|
+
import { tmpdir } from 'node:os'
|
|
70
|
+
import { join } from 'node:path'
|
|
71
|
+
|
|
72
|
+
const CRED_CS = `
|
|
73
|
+
using System;
|
|
74
|
+
using System.Runtime.InteropServices;
|
|
75
|
+
using System.Text;
|
|
76
|
+
class AGYCredDump {
|
|
77
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
78
|
+
struct CRED { public int Flags; public int Type; public IntPtr TargetName; public IntPtr Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; public int BlobSize; public IntPtr Blob; public int Persist; public int AttrCount; public IntPtr Attr; public IntPtr Alias; public IntPtr UserName; }
|
|
79
|
+
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
80
|
+
static extern bool CredRead(string target, int type, int reserved, out IntPtr cred);
|
|
81
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
82
|
+
static extern void CredFree(IntPtr p);
|
|
83
|
+
static int Main(string[] args) {
|
|
84
|
+
string target = args.Length > 0 ? args[0] : "gemini:antigravity";
|
|
85
|
+
IntPtr p;
|
|
86
|
+
if (!CredRead(target, 1, 0, out p)) { Console.Error.WriteLine("ERR credread " + Marshal.GetLastWin32Error()); return 1; }
|
|
87
|
+
try {
|
|
88
|
+
CRED c = (CRED)Marshal.PtrToStructure(p, typeof(CRED));
|
|
89
|
+
byte[] b = new byte[c.BlobSize];
|
|
90
|
+
Marshal.Copy(c.Blob, b, 0, c.BlobSize);
|
|
91
|
+
Console.OutputEncoding = Encoding.UTF8;
|
|
92
|
+
Console.WriteLine(Encoding.UTF8.GetString(b));
|
|
93
|
+
return 0;
|
|
94
|
+
} finally { CredFree(p); }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
`
|
|
98
|
+
|
|
99
|
+
function getCredDumpExe() {
|
|
100
|
+
const dir = join(tmpdir(), 'agy-quota-tool')
|
|
101
|
+
const exe = join(dir, 'agy-cred-dump.exe')
|
|
102
|
+
if (existsSync(exe)) return exe
|
|
103
|
+
mkdirSync(dir, { recursive: true })
|
|
104
|
+
const csPath = join(dir, 'agy-cred-dump.cs')
|
|
105
|
+
writeFileSync(csPath, CRED_CS, 'utf8')
|
|
106
|
+
const csc = process.env.WINDIR + '\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe'
|
|
107
|
+
const r = spawnSync(csc, ['/nologo', '/out:' + exe, csPath], { encoding: 'utf8', timeout: 30000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 })
|
|
108
|
+
if (r.status !== 0 || !existsSync(exe)) throw new Error('csc compile failed: ' + String(r.stderr || r.stdout || '').slice(0, 400))
|
|
109
|
+
return exe
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readWindowsCredential(target) {
|
|
113
|
+
const exe = getCredDumpExe()
|
|
114
|
+
const r = spawnSync(exe, [target], { encoding: 'utf8', timeout: 10000, windowsHide: true, maxBuffer: 1024 * 1024 })
|
|
115
|
+
if (r.error) throw new Error('cred-dump spawn: ' + r.error.message)
|
|
116
|
+
const out = String(r.stdout || '').trim()
|
|
117
|
+
if (r.status !== 0 || out.startsWith('ERR:')) throw new Error('CredRead failed (status=' + r.status + ') ' + String(r.stderr || '').slice(0, 200))
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 用 refresh_token 换新 access_token
|
|
122
|
+
async function refreshAccessToken(refreshToken) {
|
|
123
|
+
const body = new URLSearchParams({
|
|
124
|
+
client_id: OAUTH_CLIENT_ID,
|
|
125
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
126
|
+
refresh_token: refreshToken,
|
|
127
|
+
grant_type: 'refresh_token',
|
|
128
|
+
})
|
|
129
|
+
const res = await fetch(TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(), signal: AbortSignal.timeout(15000) })
|
|
130
|
+
if (!res.ok) throw new Error('token refresh HTTP ' + res.status + ': ' + (await res.text()).slice(0, 300))
|
|
131
|
+
const j = await res.json()
|
|
132
|
+
if (!j.access_token) throw new Error('token refresh: no access_token')
|
|
133
|
+
return j
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function postJson(url, token, payload) {
|
|
137
|
+
const res = await fetch(url, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json', 'User-Agent': USER_AGENT },
|
|
140
|
+
body: JSON.stringify(payload),
|
|
141
|
+
signal: AbortSignal.timeout(20000),
|
|
142
|
+
})
|
|
143
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
144
|
+
return res.json()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 依次尝试端点,全部失败抛错
|
|
148
|
+
async function firstOk(urls, token, payload) {
|
|
149
|
+
let lastErr = null
|
|
150
|
+
for (const u of urls) {
|
|
151
|
+
try { return await postJson(u, token, payload) } catch (e) { lastErr = e }
|
|
152
|
+
}
|
|
153
|
+
throw lastErr || new Error('all endpoints failed')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function roundPct(f) { return typeof f === 'number' ? Math.round(f * 100) : null }
|
|
157
|
+
|
|
158
|
+
// 模型家族识别:Claude / GPT 属于 3p 池子(Antigravity 上基本不可用,DSH 不应
|
|
159
|
+
// 主动选择),Gemini 是主力可用池子,其余(tab_*、chat_* 等)归 other。
|
|
160
|
+
function modelFamily(name) {
|
|
161
|
+
const n = String(name || '').toLowerCase()
|
|
162
|
+
if (n.includes('claude')) return 'claude'
|
|
163
|
+
if (n.includes('gpt')) return 'gpt'
|
|
164
|
+
if (n.includes('gemini')) return 'gemini'
|
|
165
|
+
return 'other'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function main() {
|
|
169
|
+
const summaryOnly = process.argv.includes('--summary')
|
|
170
|
+
try {
|
|
171
|
+
// 1. 读凭据
|
|
172
|
+
const blobText = readWindowsCredential(CRED_TARGET)
|
|
173
|
+
let tok
|
|
174
|
+
try { tok = JSON.parse(blobText).token } catch (e) { throw new Error('credential blob is not JSON: ' + blobText.slice(0, 120)) }
|
|
175
|
+
const refreshToken = tok && tok.refresh_token
|
|
176
|
+
if (!refreshToken) throw new Error('no refresh_token in credential')
|
|
177
|
+
// 2. 刷新 access_token
|
|
178
|
+
const fresh = await refreshAccessToken(refreshToken)
|
|
179
|
+
const accessToken = fresh.access_token
|
|
180
|
+
// 3. fetchAvailableModels(每模型池子%)
|
|
181
|
+
const quotaResp = await firstOk(CLOUDCODE_ENDPOINTS, accessToken, {})
|
|
182
|
+
const models = []
|
|
183
|
+
if (quotaResp && quotaResp.models && typeof quotaResp.models === 'object') {
|
|
184
|
+
for (const [name, info] of Object.entries(quotaResp.models)) {
|
|
185
|
+
const q = info && info.quotaInfo
|
|
186
|
+
if (q) {
|
|
187
|
+
const family = modelFamily(name)
|
|
188
|
+
models.push({ name, percentage: roundPct(q.remainingFraction), resetTime: q.resetTime || '', displayName: (info && info.displayName) || '', family, recommended: family === 'gemini' || family === 'other' })
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
models.sort((a, b) => (b.percentage || 0) - (a.percentage || 0))
|
|
192
|
+
}
|
|
193
|
+
// 4. retrieveUserQuotaSummary(分组套餐余量)
|
|
194
|
+
let groups = []
|
|
195
|
+
try {
|
|
196
|
+
const sum = await firstOk(SUMMARY_ENDPOINTS, accessToken, {})
|
|
197
|
+
if (sum && Array.isArray(sum.groups)) {
|
|
198
|
+
groups = sum.groups.map((g) => ({
|
|
199
|
+
displayName: g.displayName || '',
|
|
200
|
+
buckets: (g.buckets || []).map((b) => ({ bucketId: b.bucketId || '', window: b.window || '', remainingFraction: typeof b.remainingFraction === 'number' ? b.remainingFraction : null, resetTime: b.resetTime || '' })),
|
|
201
|
+
}))
|
|
202
|
+
}
|
|
203
|
+
} catch (e) { /* summary 失败不阻塞主结果 */ }
|
|
204
|
+
const out = { ok: true, models, groups, tier: (tok.auth_method || 'consumer'), tokenSource: 'windows-credential:gemini:antigravity' }
|
|
205
|
+
if (summaryOnly) {
|
|
206
|
+
// 一行紧凑摘要
|
|
207
|
+
const weekBuckets = groups.flatMap((g) => g.buckets.filter((b) => b.window === 'weekly').map((b) => ({ g: g.displayName, id: b.bucketId, pct: roundPct(b.remainingFraction), reset: b.resetTime })))
|
|
208
|
+
// topModels 只列推荐模型(Gemini/other),Claude/GPT 3p 不推荐故不列出。
|
|
209
|
+
const recommended = models.filter((m) => m.recommended)
|
|
210
|
+
const pool = recommended.length ? recommended : models
|
|
211
|
+
const modelSummary = pool.slice(0, 4).map((m) => m.name.split('-').slice(0, 2).join('-') + ' ' + m.percentage + '%').join(', ')
|
|
212
|
+
console.log(JSON.stringify({ ok: true, weekly: weekBuckets, topModels: modelSummary }))
|
|
213
|
+
} else {
|
|
214
|
+
console.log(JSON.stringify(out))
|
|
215
|
+
}
|
|
216
|
+
process.exit(0)
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.log(JSON.stringify({ ok: false, error: String(e && e.message || e) }))
|
|
219
|
+
process.exit(1)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
main().catch((e) => { console.log(JSON.stringify({ ok: false, error: String(e && e.message || e) })); process.exit(1) })
|