dsh-cursor-dev 0.12.0 → 0.12.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/README.md +9 -9
- package/docs/INSTALL.md +10 -10
- package/docs/skill-drafts/subagent-model-routing/SKILL.md +66 -0
- package/lib/index.js +9 -3
- package/lib/native-provider.js +18 -16
- package/lib/proxy.js +7 -9
- package/lib/quota-guard.js +113 -0
- package/lib/session-pool.js +4 -0
- package/lib/settings.js +4 -8
- package/lib/tool.js +24 -4
- package/lib/usage.js +436 -128
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ flowchart LR
|
|
|
55
55
|
3. 按 DSH 提示完成加载;涉及后台代码更新时重启 DSH,然后刷新页面。在 Cursor 面板中选择模型并保存。
|
|
56
56
|
4. 明确告诉父模型使用 `cursor_code`,或者用下面的斜杠命令派发第一个任务。
|
|
57
57
|
|
|
58
|
-
**版本说明(
|
|
58
|
+
**版本说明(0.12.2):** 本版加入额度耗尽保护:调用 `cursor_code` 或通过原生子智能体派发之前先检查 Cursor 用量,总、Auto、API 三个额度都到上限时直接拒绝并说明原因,不再把任务派出去后中途失败;用量接口暂时取不到时不拦截调用。`cursor_subagent` 现在也会生成可点击的对话子会话,面板里点运行中或已完成的行都能查看过程,不再报「没有可恢复的会话」。面板重排为额度四张卡片置顶、代理输入框移到模型选择上方、模型目录与来源说明折叠收起。DSH 兼容性仍为 `>=0.2.1-rc.2 <0.3.0-0`。插件市场已收录本插件,卡片版本随目录探测更新;npm 包名为 `dsh-cursor-dev`,实际已发布版本以 npm 的 `latest` 标签为准。要跟进后续改动请用 Git 来源;`.tgz` 是某一刻的固定快照。详见[安装来源与版本](docs/INSTALL.md#2-选择安装来源)。
|
|
59
59
|
|
|
60
60
|
运行要求:DSH `>=0.2.1-rc.2 <0.3.0-0`(已在 `0.2.1-rc.2` 上验证);Node.js `^22.19.0 || >=24.0.0`;已登录且可访问服务的 Cursor CLI。原生子智能体功能需要 DSH 的 `subagents`、`agents` 服务,后台任务还需要对应任务服务,建议使用当前 DSH 桌面版。CLI 安装方法见 [Cursor 官方文档](https://cursor.com/docs/cli)。
|
|
61
61
|
|
|
@@ -65,7 +65,7 @@ flowchart LR
|
|
|
65
65
|
| --- | --- | --- |
|
|
66
66
|
| `cursor_code` | 父模型派发完整任务,控制目录、模式、模型、会话与报告 | 工具结果回到调用它的父模型,包含答复、步骤和检测到的文件变更 |
|
|
67
67
|
| `/cursor-subagent 任务内容` | 用户直接派发一次任务;可选前缀 `-m 模型` | 完成或失败后,插件把摘要交给原父代理继续处理 |
|
|
68
|
-
| `cursor_subagent` | 当前源码新增的 DSH 原生一次性子智能体工具,可后台运行 | 使用 DSH
|
|
68
|
+
| `cursor_subagent` | 当前源码新增的 DSH 原生一次性子智能体工具,可后台运行 | 使用 DSH 标准结果及后台任务机制;父会话保留一行 External 任务,Cursor 面板里的会话行可直接打开实时镜像子对话 |
|
|
69
69
|
|
|
70
70
|
例如,在一个空测试工作区发送:
|
|
71
71
|
|
|
@@ -169,17 +169,17 @@ flowchart LR
|
|
|
169
169
|
|
|
170
170
|
### 代理设置
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
面板的「子智能体模型」卡片里有**代理**输入框,未设置时默认为 `none`(直连)。
|
|
173
173
|
|
|
174
174
|
| 取值 | 含义 |
|
|
175
175
|
| --- | --- |
|
|
176
|
-
|
|
|
177
|
-
| `
|
|
176
|
+
| `none`(默认) | 直连,并从 CLI 子进程环境中移除继承的代理地址变量 |
|
|
177
|
+
| 留空 | 自动探测:先看 `HTTPS_PROXY` 等环境变量,再看 Windows「Internet 选项」里的代理 |
|
|
178
178
|
| `http://127.0.0.1:7890` | 显式指定代理,优先于环境变量和注册表 |
|
|
179
179
|
|
|
180
180
|
**为什么需要它:** Cursor CLI 只读 `HTTPS_PROXY` / `HTTP_PROXY` / `https_proxy` / `http_proxy`——不读注册表,也不读 `ALL_PROXY`。所以一台只把代理写在「Internet 选项」里的机器,插件启动的 CLI 子进程会直连 Cursor,而直连只会拿到模型目录里的**一小部分**:本机实测 `agent --list-models` 为 **31 个**(走代理 **265 个**),ACP 会话清单为 **10 个**(走代理 **46 个**)。这就是「有些模型看不到」的全部原因。
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
需要系统代理时,主动清空输入框并保存;插件会把探测到的地址写进子进程环境(`HTTPS_PROXY`、`HTTP_PROXY` 及小写形式),并在模型卡片的「可选模型」那一行末尾显示**子进程出口**。升级时已保存的空值仍表示自动探测,不会被改成 `none`。面板保存后对后续启动的子进程生效,不必重启应用;如果填的值解析不了,那一行会直接说明。
|
|
183
183
|
|
|
184
184
|
模型卡片下方还有可搜索的模型浏览器:点一行设为面板模型,右边的开关决定它是否出现在上方下拉框里。关掉的模型只是藏起来(写入 `disabledModels`),点「保存」后生效,随时可以再打开——这样在完整目录有上百个模型时也能只留下写业务常用的几条。
|
|
185
185
|
|
|
@@ -194,7 +194,7 @@ npm install
|
|
|
194
194
|
node --test --test-force-exit --test-skip-pattern="the child agent really edits a file|a bad model is rejected|the real Cursor account returns live quota data" "test/**/*.test.mjs"
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
-
完整测试命令为 `npm test
|
|
197
|
+
完整测试命令为 `npm test`。上面的命令跳过三个真实 CLI/账号测试,不派发真实编码任务;其余 smoke 测试仍包含只读额度 HTTP 检查,因此并非完全离线。真实编码测试需要 Cursor 登录和网络,执行任务会使用该账号额度。Windows 下的 Keychain 测试因需要 POSIX shell 而跳过,不能据此认定 macOS 已验证;企业证书环境可使用 Node.js 的 `--use-system-ca`。打包使用 `npm pack` 或 `pnpm pack`,发布前按[分发检查](docs/INSTALL.md#7-维护者分发前检查)核对安装包和版本。
|
|
198
198
|
|
|
199
199
|
## English
|
|
200
200
|
|
|
@@ -220,7 +220,7 @@ See the [installation guide](docs/INSTALL.md) for detailed setup and troubleshoo
|
|
|
220
220
|
3. Restart DSH when backend changes require it, then refresh the page. Open the Cursor panel under `/plugins/dsh-cursor-dev/`, select a model and save it. The history list keeps one row per run forever, so it sits in a height-capped scroll box and folds away when you click its heading (the choice is remembered); a row's **Delete** link clears that run's index record, journal and mirrored child — the same chain `resetSession` uses — and arms on the first click so the irreversible second click is deliberate.
|
|
221
221
|
4. Ask the parent to use `cursor_code`, or send `/cursor-subagent` followed by a complete task. The command dispatches directly and hands a completion or failure summary back to the original parent.
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
Version `0.12.2` adds quota exhaustion protection: before `cursor_code` or a native subagent call starts, the plugin reads Cursor usage and refuses the call, explaining why, when the total, Auto and API buckets are all at their limit — instead of dispatching a run that later fails mid-way. A usage lookup that cannot be completed never blocks a call. `cursor_subagent` now also creates a clickable transcript child, so rows for running and finished runs open in the panel instead of reporting that no durable conversation exists. The panel puts the four quota cards first, moves the proxy input above the model picker, and folds the model catalogue and the source/egress notes. DSH compatibility remains `>=0.2.1-rc.2 <0.3.0-0`. The dsh-market catalog already lists this plugin and updates its displayed version on subsequent probes. The public npm package is `dsh-cursor-dev`; its `latest` tag identifies the published version. Use the Git source to follow later changes; a `.tgz` is a fixed snapshot.
|
|
224
224
|
|
|
225
225
|
Requirements: DSH `>=0.2.1-rc.2 <0.3.0-0` (verified against `0.2.1-rc.2`); Node.js `^22.19.0 || >=24.0.0`; an authenticated Cursor CLI account with service access. Native dispatch additionally needs DSH's `subagents` and `agents` services, and background runs need the host's job services. A current DSH desktop installation is recommended.
|
|
226
226
|
|
|
@@ -239,7 +239,7 @@ The default model order is call argument → saved panel choice → plugin confi
|
|
|
239
239
|
|
|
240
240
|
The current source registers a native DSH provider named `cursor` and a standard one-shot tool named `cursor_subagent`. Rich `cursor_code` calls also use the native lifecycle when those host services exist. The existing generic `subagent` backend remains configured as before.
|
|
241
241
|
|
|
242
|
-
Cursor executes externally. From DSH 0.2.1 on, the host writes a single non-interactive `External` row for a `cursor_subagent` run
|
|
242
|
+
Cursor executes externally. From DSH 0.2.1 on, the host writes a single non-interactive `External` row for a `cursor_subagent` run. Both entrances also create a real transcript child that the Cursor panel opens while the first turn is running. The native service keeps its own lifecycle id; the separate transcript child does not add a duplicate catalog row. Standard background runs use DSH's job result and cancellation tools. Persistent mailbox continuation through `send_message`, and Cursor as a resident Agent Teams teammate, are not implemented. Use `cursor_code` with `continueSession` for explicit follow-ups.
|
|
243
243
|
|
|
244
244
|
The parent remains responsible for reviewing the actual changes and verification evidence. File reports have scan limits and are not a substitute for final workspace checks. Cursor model availability and usage depend on the Cursor account; the parent model uses its separate DSH provider configuration.
|
|
245
245
|
|
package/docs/INSTALL.md
CHANGED
|
@@ -38,15 +38,15 @@ agent status --format json
|
|
|
38
38
|
|
|
39
39
|
## 2. 选择安装来源
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
**当前源码版本:** `0.12.2`,新增额度耗尽保护(三个额度都到上限时提前拒绝,用量取不到时不拦截),`cursor_subagent` 生成的子会话可在面板点开查看轨迹,面板重排为额度置顶、代理输入框在模型选择上方。DSH 兼容性保持 `>=0.2.1-rc.2 <0.3.0-0`。npm 包名为 `dsh-cursor-dev`,实际已发布版本以 npm 的 `latest` 标签为准。插件市场已收录本插件,卡片版本随目录探测更新。
|
|
42
42
|
|
|
43
43
|
| 你要安装的内容 | 安装来源 |
|
|
44
44
|
| --- | --- |
|
|
45
45
|
| 插件市场里的本插件 | 市场条目解析为 `github:Dclef/dsh-cursor-dev`,安装后会跟随仓库默认分支 |
|
|
46
|
-
| npm 公共源上的已发布版本 | `dsh-cursor-dev`(`0.11.0` 起可用,按包内 `files`
|
|
46
|
+
| npm 公共源上的已发布版本 | `dsh-cursor-dev`(`0.11.0` 起可用,按包内 `files` 白名单安装,版本以 npm 的 `latest` 标签为准) |
|
|
47
47
|
| 公开仓库的当前版本(推荐) | `git+https://github.com/Dclef/dsh-cursor-dev.git` |
|
|
48
48
|
| 你本地的源码目录 | 该目录的绝对路径,例如 `D:/plugins/dsh-cursor-dev` |
|
|
49
|
-
| 维护者提供的源码快照或发布包 | `.tgz` 文件的绝对路径,例如 `D:/Downloads/dsh-cursor-dev-0.12.
|
|
49
|
+
| 维护者提供的源码快照或发布包 | `.tgz` 文件的绝对路径,例如 `D:/Downloads/dsh-cursor-dev-0.12.2.tgz` |
|
|
50
50
|
|
|
51
51
|
表中本地路径是示例,需要换成实际位置。npm 来源取 registry 上发布的最新版本;Git 来源会取得仓库默认分支的最新提交;`.tgz` 是某一刻的固定快照——要跟进后续改动就用 Git 来源。
|
|
52
52
|
|
|
@@ -88,7 +88,7 @@ dsh plugin add "/home/you/plugins/dsh-cursor-dev"
|
|
|
88
88
|
安装收到的包:
|
|
89
89
|
|
|
90
90
|
```powershell
|
|
91
|
-
dsh plugin add "D:/Downloads/dsh-cursor-dev-0.12.
|
|
91
|
+
dsh plugin add "D:/Downloads/dsh-cursor-dev-0.12.2.tgz"
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
终端没有 `dsh` 命令时,使用桌面插件管理即可。不要把在任意项目目录执行 `npm install` 当成已经在 DSH 当前 profile 中启用插件。
|
|
@@ -121,7 +121,7 @@ http://127.0.0.1:19387/plugins/dsh-cursor-dev/
|
|
|
121
121
|
|
|
122
122
|
### 模型列表不全时先看代理
|
|
123
123
|
|
|
124
|
-
同一张卡片上有 **代理**
|
|
124
|
+
同一张卡片上有 **代理** 输入框,默认值为 `none`(直连);主动留空表示自动探测系统代理,填写 URL 表示显式代理。它决定的是**插件启动的 CLI 子进程**走哪条网络出口。升级时已保存的空值仍表示系统代理,不会被覆盖。
|
|
125
125
|
|
|
126
126
|
Cursor CLI 只读 `HTTPS_PROXY` / `HTTP_PROXY`(以及小写形式),不读注册表,也不读 `ALL_PROXY`。代理只写在 Windows「Internet 选项」里的机器,子进程会直连 Cursor,而直连只返回模型目录的一部分——本机实测 `agent --list-models` 为 31 个(走代理 265 个),面板真正使用的 ACP 清单为 10 个(走代理 46 个)。
|
|
127
127
|
|
|
@@ -129,8 +129,8 @@ Cursor CLI 只读 `HTTPS_PROXY` / `HTTP_PROXY`(以及小写形式),不读
|
|
|
129
129
|
|
|
130
130
|
1. 打开 `http://<你的 DSH 地址>/plugins/dsh-cursor-dev/`。
|
|
131
131
|
2. 看「子智能体模型」卡片:**可选模型** 那一行末尾会显示 **子进程出口**,例如 `http://127.0.0.1:7890 (registry:Internet Settings)`。
|
|
132
|
-
3. 如果显示 `direct (no proxy configured)
|
|
133
|
-
4. 想强制直连时填 `none
|
|
132
|
+
3. 如果显示 `direct (no proxy configured)`,而你想使用系统代理,清空 **代理** 框并保存;也可以填上地址(如 `http://127.0.0.1:7890`)使用显式代理。保存后对后续启动的子进程生效,不需要重启应用;面板会自动重新读取模型列表。
|
|
133
|
+
4. 想强制直连时填 `none`,插件会从 CLI 子进程环境中移除继承的代理地址变量。
|
|
134
134
|
|
|
135
135
|
也可以在终端里直接对照,两个数字应当与面板一致:
|
|
136
136
|
|
|
@@ -195,14 +195,14 @@ continueSession 设为 true。让 Cursor 说明上一轮做了什么,再核对
|
|
|
195
195
|
|
|
196
196
|
| 现象 | 检查与处理 |
|
|
197
197
|
| --- | --- |
|
|
198
|
-
| 按包名安装返回 404 |
|
|
198
|
+
| 按包名安装返回 404 | 确认 registry 指向 npm 公共源且包名拼写为 `dsh-cursor-dev`,或改用插件市场、上面的 Git 来源、已取得的源码目录或安装包 |
|
|
199
199
|
| DSH 报告找不到 Cursor CLI | 检查 `agent status --format json`;确认 CLI 位于 DSH 可见的 PATH 或常见安装目录,然后重启 DSH。仍找不到时,在插件配置里设置 `command` 为可启动二进制、安装根目录、`agent.cmd`,或 `"node.exe" "index.js"`(非空时只走该路径) |
|
|
200
200
|
| 未登录或账号不可用 | 在运行 DSH 的同一操作系统用户下执行 `agent login`,检查账号和网络状态 |
|
|
201
201
|
| 面板选了 Auto,实际仍是其他模型 | 当前任务不会中途切换;新版请保存“锁定面板模型”。查看返回的 `modelSource` 与 `requestedModel`,确认是否有显式调用参数 |
|
|
202
202
|
| 显示 usage limit | 检查 Cursor 账号额度和模型可用性;更换父模型不会改变 Cursor 账号额度 |
|
|
203
203
|
| 父模型没有调用插件 | 确认插件已启用、工具允许使用且父模型支持工具调用;明确要求使用 `cursor_code` 或直接用斜杠命令 |
|
|
204
204
|
| 有 `cursor_code`,没有 `cursor_subagent` | 确认所装版本包含原生接入、bundle 配置已应用,以及当前 profile 提供子智能体和任务服务;升级后重启 |
|
|
205
|
-
|
|
|
205
|
+
| 点击运行中的 `cursor_subagent` 会话只显示运行轨迹 | 更新插件后台并重启 DSH;新版从首轮运行时即建立镜像,在 Cursor 面板中点击会话键即可打开子对话。宿主仍保留一行 External 任务;旧版本留下的已完成记录可在历史会话中读回 |
|
|
206
206
|
| 更新后页面还是旧的 | 检查更新的是当前 profile 中实际加载的包,再重启 DSH 并刷新浏览器;只修改另一份源码不会自动更新所有安装副本 |
|
|
207
207
|
| 续聊没有原来的内容 | 确认父会话、`sessionLabel` 和工作区;检查是否重置过映射、删除过 Cursor 记录,或发生了加载失败回退 |
|
|
208
208
|
| 想彻底开始新任务 | 使用新 `sessionLabel`,或对原标签设置 `resetSession:true`;不要只依赖 `continueSession:false` 清空历史 |
|
|
@@ -216,7 +216,7 @@ CLI 模型切换与任务执行使用同一个全局锁。进程可以缓存和
|
|
|
216
216
|
修改文档或本地源码不会自动发布给其他用户。分发新的功能前:
|
|
217
217
|
|
|
218
218
|
1. 核对 [package.json](../package.json) 的版本与兼容性声明,为正式发布更新版本。
|
|
219
|
-
2.
|
|
219
|
+
2. 运行适当的测试;跳过真实编码任务的命令见 [README](../README.md#开发与验证),其中仍有只读网络检查。完整 `npm test` 会包含需要真实账号的 CLI 测试。
|
|
220
220
|
3. 在源码目录运行 `npm pack` 或 `pnpm pack`,检查包中包括 `lib/`、`cordis.patch.yml`、README、本文及截图。
|
|
221
221
|
4. 使用独立的 DSH profile 验证安装包:安装、登录检测、父模型派发、结果返回、续聊和取消。
|
|
222
222
|
5. 将确认过的改动推送并打 tag,或分发检查过的 `.tgz` 包。GitHub 安装需要代码已经推送;npm 包名安装需要实际完成 npm 发布。
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: subagent-model-routing
|
|
3
|
+
description: 调用 subagent、选择子智能体模型或排查 provider/model/reasoning_effort 路由错误时使用;默认省略路由字段,指定前查询目录,禁止空字符串与猜测路由。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 子智能体模型路由与参数校验
|
|
7
|
+
|
|
8
|
+
## 适用范围
|
|
9
|
+
|
|
10
|
+
调用 `subagent`、选择子智能体的提供方、模型或推理等级,或排查以下错误时使用:
|
|
11
|
+
|
|
12
|
+
- `Error: child LLM \`provider\` must be non-empty`
|
|
13
|
+
- 子智能体模型路由不被当前 Session 允许
|
|
14
|
+
- provider 与 model 不成对,或推理等级无效
|
|
15
|
+
|
|
16
|
+
本技能只约束子智能体模型参数。用户明确指定专家、专家团或其他工具时,遵守对应工具的要求。
|
|
17
|
+
|
|
18
|
+
## 调用规则
|
|
19
|
+
|
|
20
|
+
1. **使用默认模型时,省略 provider、model、reasoning_effort 三个键。** 不用空字符串、空白字符串、null 或猜测值表示默认。
|
|
21
|
+
2. **显式指定模型前,先调用 list_subagent_models。** 先查提供方,再按需查模型及其推理等级。使用返回的 ID,不从父会话模型、自身模型身份或显示名称推断。
|
|
22
|
+
3. **provider 和 model 必须成对且非空。** 只有一个字段时,先补齐可验证的信息;本意为默认时,删除全部路由字段。
|
|
23
|
+
4. **reasoning_effort 只使用目标模型公布的有效值。** 不要求固定等级时省略;不要硬套父模型等级。
|
|
24
|
+
5. **目录与当前 Session 的允许路由分别校验。** 目录存在不保证当前会话允许。如果路由被拒绝,不猜另一个提供方、不反复碰撞。默认模型满足用户要求时,删除全部路由字段重试一次;用户锁定模型时,说明具体限制,不擅自替换。
|
|
25
|
+
6. **不同工具的模型接口不能混用。** subagent 的路由、cursor_code 的 Cursor 模型、cursor_subagent 的默认配置和专家工具的专家名称不是同一接口。subagent_fork 不接受这些模型覆盖字段。
|
|
26
|
+
7. **只发送实际需要的受支持字段。** 不为填满对象而给可选字段补空字符串。若调用层强制要求这些字段,检查正确调用方式;仍无法表达省略时报告接口限制,不伪造 ID。
|
|
27
|
+
|
|
28
|
+
## 发送前检查
|
|
29
|
+
|
|
30
|
+
- 默认调用:三个可选键都不存在。
|
|
31
|
+
- 指定调用:provider/model 是已查证、成对的非空 ID。
|
|
32
|
+
- reasoning_effort 若存在,与目标模型匹配。
|
|
33
|
+
- prompt 是完整、可独立执行的任务说明;cwd 是实际工作区。
|
|
34
|
+
- 没有把父会话或其他工具的模型名称当作已验证路由。
|
|
35
|
+
|
|
36
|
+
## 默认调用示例
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"cwd": "E:/example/project",
|
|
41
|
+
"description": "核对额度显示逻辑",
|
|
42
|
+
"prompt": "只读检查当前工作区的额度规范化和显示逻辑,给出字段语义与最小修复建议,不修改文件。"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
指定模型时,查询目录后把返回的 provider/model 成对加入。仅在需要固定、且已确认有效时加入 reasoning_effort。本技能不保存固定提供方、模型或等级示例值,以免以后照抄失效。
|
|
47
|
+
|
|
48
|
+
禁止发送:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"provider": "",
|
|
53
|
+
"model": "",
|
|
54
|
+
"reasoning_effort": ""
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 失败后的处理
|
|
59
|
+
|
|
60
|
+
- provider must be non-empty:检查是否把可选字段传成空字符串;默认分支删除三个键,再调用一次。
|
|
61
|
+
- provider/model 不成对:查询目录并补齐,或切回省略全部路由字段的默认分支。
|
|
62
|
+
- 路由不允许:按会话权限限制处理,不通过改拼写绕过。
|
|
63
|
+
- 推理等级无效:查询目标模型详情;不需要固定时省略。
|
|
64
|
+
- 参数校验失败时,子智能体没有启动。只有成功结果包含子智能体 ID,才记为已启动;交付前必须收集实际结果。
|
|
65
|
+
|
|
66
|
+
修正参数后继续用户已授权的原任务;不要将工具参数错误归因于用户项目或账户额度。
|
package/lib/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { registerCursorCommand } from './command.js';
|
|
|
26
26
|
import { createMirrorHub } from './mirror.js';
|
|
27
27
|
import { createRouteFence } from './fence.js';
|
|
28
28
|
import { UsageClient, registerUsageRoutes } from './usage.js';
|
|
29
|
+
import { CursorQuotaGuard } from './quota-guard.js';
|
|
29
30
|
import { effectiveModel, readSettings, registerSettingsRoutes, resolveModelSelection } from './settings.js';
|
|
30
31
|
import { createNativeCursorBridge } from './native-provider.js';
|
|
31
32
|
import { describeProxy, proxySettingIsDirect, resolveChildProxy, withProxyEnvironment } from './proxy.js';
|
|
@@ -174,6 +175,8 @@ export function apply(ctx, config) {
|
|
|
174
175
|
// transcript Cursor actually produced. `bind` learns the `agents` service
|
|
175
176
|
// from a context that declared the dependency, because reading it off this
|
|
176
177
|
// fiber answers `undefined` while the agent loop is still activating.
|
|
178
|
+
const usage = new UsageClient({ baseUrl: config.usageBaseUrl });
|
|
179
|
+
const quotaGuard = new CursorQuotaGuard(usage);
|
|
177
180
|
const mirrors = createMirrorHub({ logger, maxChildDepth: config.maxChildDepth });
|
|
178
181
|
const pool = resolved === undefined
|
|
179
182
|
? new UnavailablePool()
|
|
@@ -195,6 +198,7 @@ export function apply(ctx, config) {
|
|
|
195
198
|
index,
|
|
196
199
|
logger,
|
|
197
200
|
mirrors,
|
|
201
|
+
quotaGuard,
|
|
198
202
|
maxScanFiles: config.maxScanFiles,
|
|
199
203
|
maxChildDepth: config.maxChildDepth,
|
|
200
204
|
unavailable,
|
|
@@ -207,7 +211,7 @@ export function apply(ctx, config) {
|
|
|
207
211
|
resolveDefaultMode: () => readSettings(dataDir).mode,
|
|
208
212
|
});
|
|
209
213
|
|
|
210
|
-
const native = createNativeCursorBridge({ execute: rawTool.execute, mirrors, maxChildDepth: config.maxChildDepth });
|
|
214
|
+
const native = createNativeCursorBridge({ execute: rawTool.execute, mirrors, quotaGuard, maxChildDepth: config.maxChildDepth });
|
|
211
215
|
const tool = { ...rawTool, execute: (args, exec) => native.execute(args, exec) };
|
|
212
216
|
const disposeTool = ctx.tools.register({ ...tool, name: config.toolName });
|
|
213
217
|
|
|
@@ -256,6 +260,8 @@ export function apply(ctx, config) {
|
|
|
256
260
|
dataDir,
|
|
257
261
|
logger,
|
|
258
262
|
mirrors,
|
|
263
|
+
usage,
|
|
264
|
+
quotaGuard,
|
|
259
265
|
// The status route asks the CLI whether the account is signed in, which
|
|
260
266
|
// means spawning it — same environment the ACP child gets.
|
|
261
267
|
env: childEnvironment,
|
|
@@ -304,9 +310,9 @@ function mountHttpSurfaces(ctx, config, deps) {
|
|
|
304
310
|
// the dashboard RPC itself and serves both a JSON view and a
|
|
305
311
|
// self-contained HTML dashboard.
|
|
306
312
|
if (config.usageUi === true) {
|
|
307
|
-
const usage = new UsageClient({ baseUrl: config.usageBaseUrl });
|
|
308
313
|
disposers.push(registerUsageRoutes(webCtx, {
|
|
309
|
-
usage,
|
|
314
|
+
usage: deps.usage,
|
|
315
|
+
quotaGuard: deps.quotaGuard,
|
|
310
316
|
pool: deps.pool,
|
|
311
317
|
journal: deps.journal,
|
|
312
318
|
index: deps.index,
|
package/lib/native-provider.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isAbsolute } from 'node:path';
|
|
|
5
5
|
import { stat } from 'node:fs/promises';
|
|
6
6
|
import { renderCursorReport } from './tool.js';
|
|
7
7
|
import { resolveChildDepth } from './mirror.js';
|
|
8
|
+
import { quotaBlockedMessage } from './quota-guard.js';
|
|
8
9
|
|
|
9
10
|
export const CURSOR_PROVIDER = 'cursor';
|
|
10
11
|
|
|
@@ -33,7 +34,7 @@ function failure(error, cancelled = false) {
|
|
|
33
34
|
|
|
34
35
|
/** Generic native calls use the panel settings and get their own conversation.
|
|
35
36
|
* The cursor_code adapter supplies provider-private arguments for rich controls. */
|
|
36
|
-
export function createCursorProvider({ execute, mirrors, maxChildDepth }) {
|
|
37
|
+
export function createCursorProvider({ execute, mirrors, quotaGuard, maxChildDepth }) {
|
|
37
38
|
const active = new Set();
|
|
38
39
|
let closed = false;
|
|
39
40
|
return {
|
|
@@ -52,15 +53,17 @@ export function createCursorProvider({ execute, mirrors, maxChildDepth }) {
|
|
|
52
53
|
throw new Error('cursor: native prompts must contain text blocks only');
|
|
53
54
|
const task = request.prompt.map(block => block.text).join('\n\n').trim();
|
|
54
55
|
if (!task) throw new Error('cursor: native prompt must not be empty');
|
|
56
|
+
const quotaStatus = await quotaGuard?.check?.();
|
|
57
|
+
if (quotaStatus?.blocked === true)
|
|
58
|
+
throw new Error(quotaBlockedMessage(quotaStatus));
|
|
55
59
|
const originalParent = request.parent;
|
|
56
60
|
// The recursion budget is the plugin's own now that the host leaves it
|
|
57
61
|
// to the provider (`maxDepth: provider-managed`); refuse before any
|
|
58
62
|
// process, child row, or lifecycle event exists.
|
|
59
63
|
if (typeof maxChildDepth === 'number') resolveChildDepth(originalParent, maxChildDepth);
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// belongs to the cursor_code bridge, which starts the provider directly.
|
|
64
|
+
// Keep the service's external lifecycle id separate from the local
|
|
65
|
+
// transcript child. The mirror is opened explicitly by the panel
|
|
66
|
+
// and does not publish another catalog row.
|
|
64
67
|
const viaService = request.cursor === undefined;
|
|
65
68
|
const args = request.cursor?.args ?? { task, sessionLabel: `native-${randomUUID()}` };
|
|
66
69
|
const requestedCwd = typeof request.cwd === 'string' && request.cwd !== ''
|
|
@@ -82,15 +85,14 @@ export function createCursorProvider({ execute, mirrors, maxChildDepth }) {
|
|
|
82
85
|
let settled = false;
|
|
83
86
|
let disposal;
|
|
84
87
|
const cursorLifecycle = {
|
|
85
|
-
descriptor
|
|
88
|
+
// The service descriptor belongs to its external row.
|
|
89
|
+
descriptor: viaService ? undefined : request.descriptor,
|
|
86
90
|
label: request.label,
|
|
87
91
|
acquired(session) { leasedSession = session; },
|
|
88
|
-
|
|
89
|
-
opened
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
}),
|
|
92
|
+
opened(entry, key) {
|
|
93
|
+
opened = { entry, key };
|
|
94
|
+
ready.resolve();
|
|
95
|
+
},
|
|
94
96
|
};
|
|
95
97
|
const execution = Promise.resolve().then(() => execute({ ...args, workspaceRoot: cwd }, {
|
|
96
98
|
agent: originalParent,
|
|
@@ -125,8 +127,8 @@ export function createCursorProvider({ execute, mirrors, maxChildDepth }) {
|
|
|
125
127
|
}
|
|
126
128
|
})().finally(() => active.delete(dispose));
|
|
127
129
|
active.add(dispose);
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
+
// Publish the external lifecycle immediately, including while queued.
|
|
131
|
+
// Its id stays distinct from the panel's real transcript child.
|
|
130
132
|
if (viaService)
|
|
131
133
|
return { id: `cursor-${randomUUID()}`, result, cursorReport, dispose };
|
|
132
134
|
try {
|
|
@@ -168,13 +170,13 @@ function announceMirror(parent, child, label) {
|
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
/** Late-bound native service; absence preserves the existing standalone tool. */
|
|
171
|
-
export function createNativeCursorBridge({ execute, mirrors, maxChildDepth }) {
|
|
173
|
+
export function createNativeCursorBridge({ execute, mirrors, quotaGuard, maxChildDepth }) {
|
|
172
174
|
let binding;
|
|
173
175
|
return {
|
|
174
176
|
bind(ctx) {
|
|
175
177
|
const runtime = ctx.get?.('subagents');
|
|
176
178
|
if (typeof runtime?.registerProvider !== 'function') return;
|
|
177
|
-
const provider = createCursorProvider({ execute, mirrors, maxChildDepth });
|
|
179
|
+
const provider = createCursorProvider({ execute, mirrors, quotaGuard, maxChildDepth });
|
|
178
180
|
const unregister = runtime.registerProvider(provider);
|
|
179
181
|
const current = { runtime, provider };
|
|
180
182
|
binding = current;
|
package/lib/proxy.js
CHANGED
|
@@ -371,15 +371,16 @@ export function proxyEnvironment(proxy) {
|
|
|
371
371
|
/**
|
|
372
372
|
* `env` with the proxy variables merged in.
|
|
373
373
|
*
|
|
374
|
-
* A direct connection
|
|
375
|
-
*
|
|
374
|
+
* A direct connection removes inherited proxy variables without changing the
|
|
375
|
+
* caller's environment.
|
|
376
376
|
*
|
|
377
377
|
* @param env - the environment to start from.
|
|
378
378
|
* @param proxy - a resolved proxy, or undefined for a direct connection.
|
|
379
379
|
*/
|
|
380
380
|
export function withProxyEnvironment(env, proxy) {
|
|
381
381
|
const additions = proxyEnvironment(proxy);
|
|
382
|
-
if (Object.keys(additions).length === 0)
|
|
382
|
+
if (Object.keys(additions).length === 0)
|
|
383
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !PROXY_ENV_KEYS.includes(key)));
|
|
383
384
|
return { ...env, ...additions };
|
|
384
385
|
}
|
|
385
386
|
|
|
@@ -389,11 +390,8 @@ export function withProxyEnvironment(env, proxy) {
|
|
|
389
390
|
* The panel's `proxy` setting drives this, and its three kinds of answer mean
|
|
390
391
|
* genuinely different things:
|
|
391
392
|
*
|
|
392
|
-
* - `
|
|
393
|
-
*
|
|
394
|
-
* the default is not "direct".
|
|
395
|
-
* - `none` (also `direct`, `off`, `no`) is the escape hatch for a machine
|
|
396
|
-
* whose registry proxy is stale or wrong.
|
|
393
|
+
* - `none` (the default; also `direct`, `off`, `no`) connects directly.
|
|
394
|
+
* - `''` is AUTO: ask the environment, then the machine.
|
|
397
395
|
* - anything else is taken literally as a proxy endpoint, in every shape
|
|
398
396
|
* {@link parseProxy} accepts.
|
|
399
397
|
*
|
|
@@ -402,7 +400,7 @@ export function withProxyEnvironment(env, proxy) {
|
|
|
402
400
|
* @returns the resolved proxy, or undefined for a direct connection.
|
|
403
401
|
*/
|
|
404
402
|
export async function resolveChildProxy(configured, options = {}) {
|
|
405
|
-
const value = typeof configured === 'string' ? configured.trim() : '';
|
|
403
|
+
const value = typeof configured === 'string' ? configured.trim() : 'none';
|
|
406
404
|
if (DIRECT_PROXY_VALUES.has(value.toLowerCase())) return undefined;
|
|
407
405
|
if (value.length === 0) return await readSystemProxy(options);
|
|
408
406
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const RECHECK_MS = 30_000;
|
|
2
|
+
|
|
3
|
+
const QUOTA_ERROR = /\b(?:resource[_ -]?exhausted|insufficient[_ -]?quota|quota[_ -]?exceeded|credit[_ -]?balance[_ -]?exhausted)\b|\b(?:insufficient|out of|no remaining|zero|empty|exhausted|depleted)\b.{0,50}\b(?:cursor\s+)?(?:usage|quota|credits?|balance)\b|\b(?:cursor\s+)?(?:usage|quota|credits?|balance)\b.{0,50}\b(?:exhausted|depleted|used up|is empty|limit (?:has been )?reached|limit exceeded|insufficient)\b|\b(?:you(?:'ve| have) hit your usage limit|usage limit has been reached|no usage credits remaining)\b/i;
|
|
4
|
+
|
|
5
|
+
/** Only the explicit upstream bucket percentages can establish global exhaustion. */
|
|
6
|
+
export function usageQuotaExhausted(usage) {
|
|
7
|
+
const percentages = ['total', 'auto', 'api'].map(name => usage?.[name]?.percentUsed);
|
|
8
|
+
return percentages.every(value => typeof value === 'number' && Number.isFinite(value) && value >= 100);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Inspect an actual Cursor/ACP failure, never the dashboard's advisory prose. */
|
|
12
|
+
export function cursorQuotaFailure(error) {
|
|
13
|
+
const code = error?.code;
|
|
14
|
+
if (code === 8 || /^(?:RESOURCE_EXHAUSTED|INSUFFICIENT_QUOTA|QUOTA_EXCEEDED|CREDIT_BALANCE_EXHAUSTED)$/i.test(String(code ?? '')))
|
|
15
|
+
return true;
|
|
16
|
+
const parts = [error?.name, error?.message, error?.data?.message, error?.cause?.message];
|
|
17
|
+
if (typeof error === 'string') parts.push(error);
|
|
18
|
+
try {
|
|
19
|
+
if (error !== null && typeof error === 'object') parts.push(JSON.stringify(error));
|
|
20
|
+
}
|
|
21
|
+
catch { /* An unstringifiable error can still match its message/code. */ }
|
|
22
|
+
return QUOTA_ERROR.test(parts.filter(value => typeof value === 'string').join(' '));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function quotaBlockedMessage(state) {
|
|
26
|
+
return state?.source === 'cursor-error'
|
|
27
|
+
? 'CURSOR_QUOTA_EXHAUSTED: Cursor reported that its usage is exhausted. New Cursor child-agent runs are paused until a successful quota refresh confirms capacity has returned.'
|
|
28
|
+
: 'CURSOR_QUOTA_EXHAUSTED: Cursor reports that all usage buckets are exhausted. No child agent was started. Check the quota panel and retry after a successful refresh confirms capacity has returned.';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Shared gate for native subagent starts and direct cursor_code calls. */
|
|
32
|
+
export class CursorQuotaGuard {
|
|
33
|
+
#usage;
|
|
34
|
+
#now;
|
|
35
|
+
#recheckMs;
|
|
36
|
+
#state = { blocked: false, source: 'unknown' };
|
|
37
|
+
#lastCheckAt = 0;
|
|
38
|
+
#revision = 0;
|
|
39
|
+
#inflight;
|
|
40
|
+
|
|
41
|
+
constructor(usage, options = {}) {
|
|
42
|
+
this.#usage = usage;
|
|
43
|
+
this.#now = options.now ?? Date.now;
|
|
44
|
+
this.#recheckMs = options.recheckMs ?? RECHECK_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
snapshot() {
|
|
48
|
+
return { ...this.#state };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async check() {
|
|
52
|
+
const now = this.#now();
|
|
53
|
+
if (this.#state.blocked && now - this.#lastCheckAt < this.#recheckMs)
|
|
54
|
+
return this.snapshot();
|
|
55
|
+
if (this.#inflight !== undefined)
|
|
56
|
+
return this.#inflight;
|
|
57
|
+
|
|
58
|
+
const revision = this.#revision;
|
|
59
|
+
this.#inflight = (async () => {
|
|
60
|
+
let snapshot;
|
|
61
|
+
try {
|
|
62
|
+
snapshot = await this.#usage?.get?.({ force: this.#state.blocked });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
snapshot = undefined;
|
|
66
|
+
}
|
|
67
|
+
this.#lastCheckAt = this.#now();
|
|
68
|
+
if (revision !== this.#revision && this.#state.blocked)
|
|
69
|
+
return this.snapshot();
|
|
70
|
+
return this.observeUsage(snapshot);
|
|
71
|
+
})().finally(() => { this.#inflight = undefined; });
|
|
72
|
+
return this.#inflight;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
observeUsage(snapshot) {
|
|
76
|
+
if (snapshot?.ok !== true) {
|
|
77
|
+
if (!this.#state.blocked)
|
|
78
|
+
this.#state = { blocked: false, source: 'unknown' };
|
|
79
|
+
return this.snapshot();
|
|
80
|
+
}
|
|
81
|
+
const fetchedAt = snapshot.fetchedAt;
|
|
82
|
+
if (this.#state.blocked
|
|
83
|
+
&& (!Number.isFinite(fetchedAt) || fetchedAt <= this.#state.since))
|
|
84
|
+
return this.snapshot();
|
|
85
|
+
this.#lastCheckAt = this.#now();
|
|
86
|
+
if (usageQuotaExhausted(snapshot.usage)) {
|
|
87
|
+
this.#state = {
|
|
88
|
+
blocked: true,
|
|
89
|
+
source: 'usage',
|
|
90
|
+
reason: 'all-usage-buckets-exhausted',
|
|
91
|
+
since: Number.isFinite(fetchedAt) ? fetchedAt : this.#now(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.#state = { blocked: false, source: 'usage' };
|
|
96
|
+
}
|
|
97
|
+
return this.snapshot();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
observeFailure(error) {
|
|
101
|
+
if (!cursorQuotaFailure(error))
|
|
102
|
+
return this.snapshot();
|
|
103
|
+
this.#revision += 1;
|
|
104
|
+
this.#lastCheckAt = this.#now();
|
|
105
|
+
this.#state = {
|
|
106
|
+
blocked: true,
|
|
107
|
+
source: 'cursor-error',
|
|
108
|
+
reason: 'cursor-reported-exhaustion',
|
|
109
|
+
since: this.#lastCheckAt,
|
|
110
|
+
};
|
|
111
|
+
return this.snapshot();
|
|
112
|
+
}
|
|
113
|
+
}
|
package/lib/session-pool.js
CHANGED
|
@@ -57,6 +57,7 @@ export class SessionPool {
|
|
|
57
57
|
/** Tail of the FIFO queue that serializes every Cursor turn. */
|
|
58
58
|
#gate = Promise.resolve();
|
|
59
59
|
#lockDepth = 0;
|
|
60
|
+
#activeKey;
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* @param config - plugin config (maxLiveSessions, sessionIdleMinutes, spawn/env).
|
|
@@ -99,6 +100,7 @@ export class SessionPool {
|
|
|
99
100
|
modelChoice: entry.modelChoice,
|
|
100
101
|
mode: entry.session.modes?.currentModeId,
|
|
101
102
|
turns: entry.turns.length,
|
|
103
|
+
running: entry.key === this.#activeKey,
|
|
102
104
|
reloaded: entry.reloaded,
|
|
103
105
|
idleMs: Date.now() - entry.lastUsed,
|
|
104
106
|
alive: entry.session.alive,
|
|
@@ -357,6 +359,7 @@ export class SessionPool {
|
|
|
357
359
|
*/
|
|
358
360
|
async run(options, body) {
|
|
359
361
|
const release = await this.#enter(options.signal);
|
|
362
|
+
this.#activeKey = options.key;
|
|
360
363
|
try {
|
|
361
364
|
options.signal?.throwIfAborted();
|
|
362
365
|
const lease = await this.#acquire(options, options.signal);
|
|
@@ -373,6 +376,7 @@ export class SessionPool {
|
|
|
373
376
|
}
|
|
374
377
|
}
|
|
375
378
|
finally {
|
|
379
|
+
this.#activeKey = undefined;
|
|
376
380
|
release();
|
|
377
381
|
}
|
|
378
382
|
}
|
package/lib/settings.js
CHANGED
|
@@ -57,11 +57,7 @@ export function emptySettings() {
|
|
|
57
57
|
modelPolicy: 'per-call',
|
|
58
58
|
mode: '',
|
|
59
59
|
language: '',
|
|
60
|
-
|
|
61
|
-
// proxy. That default is the whole point: the child CLI does not read
|
|
62
|
-
// the registry itself, so without this it lists only the models the
|
|
63
|
-
// direct route publishes.
|
|
64
|
-
proxy: '',
|
|
60
|
+
proxy: 'none',
|
|
65
61
|
// Models the operator switched off in the picker. A DENY list, not an
|
|
66
62
|
// allow list, so a catalogue that gains entries keeps showing them: an
|
|
67
63
|
// empty list means "everything the CLI knows about" and a fresh install
|
|
@@ -577,16 +573,16 @@ const MAX_PROXY_CHARS = 200;
|
|
|
577
573
|
/**
|
|
578
574
|
* Coerce the `proxy` setting.
|
|
579
575
|
*
|
|
580
|
-
* `
|
|
576
|
+
* `none` is the default and means never proxy; `''` means auto-detect; anything
|
|
581
577
|
* else is an endpoint. Only the shape of the value is checked here — whether it
|
|
582
578
|
* parses is `resolveChildProxy`'s job, and it can report that failure without
|
|
583
579
|
* the settings file having to be valid first. An over-long value is dropped
|
|
584
580
|
* rather than truncated, because half a URL is worse than none.
|
|
585
581
|
*/
|
|
586
582
|
function proxySetting(value) {
|
|
587
|
-
if (typeof value !== 'string') return '';
|
|
583
|
+
if (typeof value !== 'string') return 'none';
|
|
588
584
|
const text = value.trim();
|
|
589
|
-
return text.length > MAX_PROXY_CHARS ? '' : text;
|
|
585
|
+
return text.length > MAX_PROXY_CHARS ? 'none' : text;
|
|
590
586
|
}
|
|
591
587
|
|
|
592
588
|
/** Longest model id accepted in the picker's deny list; ids are short by nature. */
|