@eddyskywalker/dsh-chatgpt-subscription 0.3.0 → 0.3.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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - **子代理必须写明模型**:授权守卫不再只拒绝「写明且不在白名单内」的路由,而是要求带有允许列表的会话里每次委派都成对给出 `provider` + `model`。此前不写路由的调用会让子代理继承父级模型(设置卡白名单形同虚设,子代理总是跑在主模型上);现在缺省与只写一半都会被拒绝,拒绝理由里给出全部已授权路由并提示先用 `list_subagent_models` 查询。只读取公开接口(`ctx.tools.guard` + 会话日志 + 设置文档),不修改 DSH 本体。
6
+ - 授权相关拒绝文案拆分为「未指定路由」「只指定一半」「路由不在列表内」三种,`delegationDenialReason` 不再回退到父级 `options` 判定继承路由;新增/改写 `test/subagent-model-authorization.test.ts` 用例覆盖这三种拒绝,以及未记录策略与非托管工具名保持放行。
7
+ - 确认 `run_code`(代码模式)无法绕过该守卫:程序里通过 SDK 调用的 `tools["subagent"]` 走的是同一套 `prepare → guard → dispatch` 调度流水线,守卫在调度入口拦截,拒绝理由以 `ToolCallError` 抛回程序。新增 `test/subagent-model-authorization-ptc.test.ts`(3 条)用真实 `ToolRuntime`(code 模式)+ 假 `CodeRuntime` 驱动绑定函数,覆盖「已授权放行 / 缺省拒绝 / 越权拒绝」;这是实测而非假设(先看 DSH 源码确认嵌套子调用确实复用同一调度器,再用用例锁定行为)。
8
+
5
9
  - 新增 Kimi Code(Kimi For Coding 订阅)线路:注册 `kimi-code` Provider,把 Moonshot 的 Kimi Code 订阅作为本插件第四条线路接入 DSH。Kimi Code 与 Moonshot 开放平台(pay-as-you-go)是**两套互不通用的系统**:订阅走 `https://api.kimi.com/coding/v1`、凭据来自 `auth.kimi.com` 的 OAuth;开放平台的 key 与 base URL 在订阅端会被判为 `401 Invalid Authentication`,插件据此把两者严格分开。
6
10
  - OAuth 采用 RFC 8628 设备码流程(`src/host/kimi-code/oauth.ts`),复刻官方 CLI 的协议细节:`POST /api/oauth/device_authorization` 只带 `client_id`(公共客户端,**无 client secret、无 PKCE、无 scope**),`POST /api/oauth/token` 轮询用 `grant_type=urn:ietf:params:oauth:grant-type:device_code`;令牌响应字段(`access_token` / `refresh_token` / `expires_in` / `scope` / `token_type`)与官方实现逐字段对应。`slow_down` 按 RFC 把轮询间隔永久 `+5s`;`authorization_pending` 继续等待;`expired_token` **不当作失败**,而是像官方 CLI 一样重新申请设备码(用户授权慢了仍能登入);`access_denied` 单独分类。设置卡展示用户码、一次性链接与到期时间,可复制用户码、可取消。
7
11
  - 令牌自动续期:阈值取 `max(300s, expires_in × 0.5)`(与官方一致),同一进程内并发调用**共用一次刷新请求**(避免订阅侧并发轮换同一 refresh token);被拒的 refresh token 记入进程级 tombstone 并进入 5 分钟冷却,之后直接提示重新登录而不是反复打扰服务端。
@@ -70,6 +74,32 @@
70
74
  - 声明所在 system 消息**同时带有文本**时不再静默丢弃:服务的动态工具 schema 没有 `content` 字段,两者无法合成一条消息,因此现在保留文本(另发一条),并把声明替换为明确的说明消息,而不是让工具无声消失。
71
75
  - **按官方 CLI 的能力表逐模型修正两项能力**(依据 `managed:kimi-code` 托管模型表里每个模型的 `capabilities` 列表):`k3` = image_in + video_in + dynamically_loaded_tools;`k3-256k` = image_in + **dynamically_loaded_tools**(无 video);`kimi-for-coding` = image_in + video_in + **dynamically_loaded_tools**;`kimi-for-coding-highspeed` = image_in + video_in(**无** dynamically_loaded_tools)。修正了先前把 `dynamically_loaded_tools` 当成「K3 独有」的推导错误——官方线文档只提 K3 是因为它描述的是 K3 的请求 schema,而 CLI 自己的能力表把它也标给了 K2.8 Preview。
72
76
  - 修正能力判定与官方表格的一致性(已对照 https://www.kimi.com/code/docs/en/kimi-code/models.html 逐项核对):`k3` 与 `kimi-for-coding` 为「Image, video」、`k3-256k` 为「Image only」,与内置表一致;`kimi-for-coding-highspeed` 官方标为 K2.7 Code HighSpeed,「Thinking: ON」且无可选档位,故其固有档位仍按官方标注为 `high`。
77
+ - 按第二轮审计修正视频入口的几处问题:
78
+ - **`.mkv` 能存不能发**:入口白名单收 `.mkv`(存为 `video/x-matroska`)而 mapper 的 `isVideoMediaType` 不含它,导致用户挂载 mkv 会收到「Attached video ✅」,模型却只拿到 `unsupported-container` 占位文本。现在入口**只接受 mapper 真能发出的容器**(即 `KIMI_VIDEO_MEDIA_TYPES`),mkv 在入口即被拒绝并说明——入口承诺与线上行为从此是同一件事。
79
+ - **`e2e` 注释的覆盖声称超出实际**:该测试直调 `buildOpenAIRequest`,从不经过 DSH 真实管道。注释已改为如实说明「覆盖请求映射阶段 + 安装版运行时的内容助手」,并明确列出**未**覆盖的部分(会话持久化 / compaction / transcript),要求装机实测。过程中还发现一个值得记录的事实:本仓库**安装版 `@deepseek-ai/dsh-llm` 是 0.1.1-rc.2**(根 barrel 只导出 `contentHasImage` / `projectImagesForTextModel`,**完全没有** file 投影),而工作区 checkout 是 0.1.5-rc.1——两者不是同一份代码,断言已改为针对实际运行的那份。
80
+ - **视频存储新增回收**:内容寻址让重复挂载免费,但此前没有任何清理,目录只会增长。现在写入时按 mtime 做 LRU 回收(预算 512 MB,best-effort、失败不影响挂载),并顺带清理崩溃写入残留的 `.tmp.*` 文件。
81
+ - 顺带修掉两处小瑕疵:`video-tool.ts` 自己写的 DNS `lookup` 改为复用 `fetch-address-policy.ts` 已有的 `lookupHostAddresses`(避免语义漂移);`index.ts` 中 `disposeVideoTool()` 的缩进与相邻一致。
82
+ - **视频输入打通了入口**:此前只有一条没有生产者的 mapper 路径(能力表也只能标注「无上传入口」)。现在新增两件东西,让视频端到端可达:
83
+ - `kimi_attach_video` 工具(`src/host/kimi-code/video-tool.ts`):接受**本地绝对路径**或 **http(s) 链接**,把视频字节交给插件的视频存储,再以 `exec.deferContext()` 注入一条 plugin 来源的 user 消息(与本插件已有的图片工具同一机制,**不需要改动 DSH**)。可选 `question` 参数让模型在同一轮就视频作答。
84
+ - 视频本地存储(`src/host/kimi-code/video-store.ts`):DSH 的附件服务只存图片,因此本线路自带存储。标识取字节 sha256(重复挂载同一文件幂等),读取时**重新校验摘要**,被篡改或截断的对象会被拒绝而不是当成原文件发出去。
85
+ - **尺寸上限取官方依据**:官方视频集成把本地文件编码为 `data:video/...;base64,...` 并限制该载荷约 **50 MB**(且明确说这是其客户端上限、非 Kimi API 上限),VS Code 端文件选择器限 **20 MB**。前者描述的是「这个 wire 上模型接受什么」,故上限设为略低于它的 **30 MB 原始字节**(base64 增长 4/3,所以编码后正好落在 40 MB,留在 50 MB 之下)。
86
+ - **两个安全/正确性闸门**:URL 来源在发起请求**之前**套用与搜索抓取 provider 相同的公网地址策略(否则该工具会成为一个 SSRF 原语——测试里有一条专门证明策略缺失时用例会失败);且只有当**当前会话路由到 `kimi-code` 且所选模型声明了 video** 时才允许挂载,否则明确拒绝并给出补救(换 k3 / kimi-for-coding),绝不会把别的适配器没有处理分支的块注入进去。
87
+ - 能力表脚注相应改为「视频需先用 kimi_attach_video 挂载;直接粘贴仍只支持图片」。
88
+ - 新增 `test/kimi-code-video-tool.test.ts`(17 条:容器识别、内容寻址与幂等、超限/空文件/类型拒绝、摘要失配与文件缺失、相对路径/未知扩展名/非视频模型/错误 provider 的拒绝、私网 URL 拒绝且**不发出请求**)与 `test/kimi-code-video-e2e.test.ts`(3 条:从落盘文件解析出字节并确认线上是真实 base64 内容、字节缺失时降级为可读文本、无视频请求不受影响)。
89
+ - **修复审查发现的问题**(视频与动态工具):
90
+ - **live 目录现在真的能关掉能力**:`parseCatalogModel` 原先把 `supports_dynamic_tools === true` 之外的一切都当作「字段缺席」,于是服务端显式返回 `false` 时会回退到内置表、照样显示支持——与 `model-catalog.ts` 注释里「live 列表权威、包括可以关掉」的承诺自相矛盾。现按三态解析(`true` / `false` / 缺席),显式 `false` 穿透回退。
91
+ - **卡片与请求路径统一到一个解析入口** `dynamicToolsForEntry()`:此前卡片会回退内置表而请求路径 (`entry?.supportsDynamicTools === true`) 不会,导致在线但 `/models` 未返回该字段时 **UI 显示支持、实际请求却降级为「模型不支持」**,且行为随网络状态翻转;离线反而正常。现在两处共用同一函数。
92
+ - **声明不再随持久化丢失**:`withMessageTools` 原先把声明只挂在 `Symbol` 上且 `enumerable: false`,而 DSH 会话历史经 JSON 持久化必然丢掉符号键——会话恢复后声明会静默消失,模型会「以为自己有工具但请求里没有」。现在同时写入一个普通字符串键 `kimiCodeMessageTools`(可被 JSON 序列化,但仍非枚举,兄弟线路照样看不见),并新增 `rehydrateMessageTools()` 供恢复后重新挂上符号。
93
+ - **不再重复发送 system 文本**:`leadingSystemText` 折叠所有 system 文本到请求开头,而声明槽位又会在原位置重发一次,同一段文本出现两遍(浪费 token,且第二次位置可能扰动本想保护的缓存前缀)。现在 `leadingSystemText` 跳过带声明的消息,文本只在声明位置出现一次。
94
+ - **Anthropic 线路不再静默吞掉声明**:该协议不支持消息级声明,原本直接丢弃且无任何提示,模型可能调用从未声明的工具。现在把未发送数量写进 `system` 提示。
95
+ - **请求体守卫不再二次序列化、也不再被用户文本欺骗**:原先用 `JSON.stringify(body).includes('"video_url"')` 判断是否带视频——body 可达数十 MB 却被序列化两次,且用户消息里只要出现该字面量就会把 2 MB 守卫放宽到 64 MB。现由调用方显式传 `carriesVideo`(复用已有的 `requestHasVideo`)。
96
+ - **不再超前宣称视频能力**:模型确实接受视频,但本插件与 DSH 的附件服务都没有视频生产者/读取者(`videos` 读取器从未在 `src/index.ts` 注入),实际永远走 `unreadable` 占位。能力表因此把标签标为「视频*」并加脚注说明当前版本没有上传入口、不会发出视频内容块,避免 UI 承诺与实际可达路径不符。
97
+ - 清理 `classifyKimiFailure` 中 `isLimit ? 'PROVIDER_ERROR' : 'PROVIDER_ERROR'` 的死三元(两分支同值,读起来像意图未实现)。
98
+ - **修复上一轮修复引入的回归**:`leadingSystemText` 是两条 wire 共用的,跳过声明载体后,只有 OpenAI 路径会在载体原位置重发文本;Anthropic 路径因此**整段丢失载体文本**(只剩工具数量提示),恰好违反 `declarationSlots` 注释里「丢掉 system 文本会静默改变模型收到的信息」这条原则。现在 Anthropic 路径会把载体文本拼回 `system`(排在 notice 之前),并加了 2 条回归用例——其中顺序断言特意先 `>= 0` 再比较,否则文本缺失时 `indexOf` 返回 -1 会让断言**空过**。
99
+ - 顺手清理审查指出的三处小瑕疵:`classifyKimiFailure` 中删掉三元后遗留的未引用 `isLimit`(改为真正参与文案选择的 `limitReached`);`client.ts` 里错位堆在 `dynamicToolsForEntry` 上方的「Input modalities」注释归位;`withMessageTools` 的注释原先一边说「非枚举所以其他读者看不到」一边字符串键副本是 `enumerable: true`,改为明确写清两个副本可见性不同及其原因。
100
+ - `assertRequestBodyFits` 现在**返回**它序列化出的 body,供适配器直接复用:此前带视频时同一个数十 MB 的 body 会被 `JSON.stringify` 两次。
101
+ - 新增 `test/kimi-code-review-fixes.test.ts`逐条钉住上述缺陷:显式 `false` 生效、三态回退边界、文本只出现一次、Anthropic 提示、守卫不被文本欺骗、声明经 JSON 往返后仍可发送;`test/kimi-code-capability-ui.test.tsx` 增加脚注相关 1 条。
102
+
73
103
  - **重排模型能力展示**:此前把能力说明当成长句塞在模型名那一列,把名称列撑开、右侧描述错位。现改为独立的四列表格(模型 / 多模态 / 动态工具 / 说明):能力只显示短标签(视频 / 仅图片 / 动态工具 / —),逐模型的协议、默认思考档位与所需套餐移入悬停提示;表格自身不再附带任何解释段落。表格抽成可测组件 `KimiModelCapabilities`(`src/client/kimi-code/KimiModelCapabilities.tsx`),新增 `test/kimi-code-capability-ui.test.tsx`(5 条)钉住列数、每模型一行、长句不得进入单元格、悬停内容,以及说明为空时不渲染 `null`。该表也**不再依赖 `description` 是否存在**——实时目录条目缺描述时整个表格(含能力)仍渲染。
74
104
  - 澄清并测试这两项能力的**跨模型隔离**:机制本身有三重隔离——符号载体**不可枚举**(其他线路的序列化器看不到它)、映射器只在 `messageTools === true` 时输出、且声明只存在于 Kimi 的 OpenAI 线路映射中。新增 `test/kimi-code-capability-isolation.test.ts`(6 条):Command Code 各模型仍不含 video(证明模块增强是**纯类型、不产生运行时值**)、Kimi 四个模型 id 的 video 与 dynamically_loaded_tools 与官方能力表**逐项**一致(并断言两者并非同一集合:HighSpeed 有 video 却无动态工具)、视频块不会出现在兄弟线路的请求体里、同一段历史里的声明也不会被兄弟线路带出去。
75
105
  - 另记录一个**证据取舍**:公共 `models.dev` 目录虽声明了 `dynamically_loaded_tools` 字段,但 Moonshot 自家四个条目均未标注,故本插件**不采信该目录**,改以官方 CLI 托管模型表的 `capabilities` 为准;运行时仍以实时 `/v1/models` 的 `supports_dynamic_tools` / `supports_video_in` 覆盖内置表。
package/README.md CHANGED
@@ -181,16 +181,18 @@ DSH 模型选择器应显示 **“Codex(ChatGPT 订阅)”**。6 Astra 与 G
181
181
 
182
182
  > Windows 文件只能由创建它的用户通过 DPAPI 解密。macOS 凭据由登录钥匙串在本机加密保存。Linux 文件是未额外加密的 JSON,依赖目录 `0700` 和文件 `0600` 隔离;不要复制、打印或提交该文件。跨平台迁移需要重新登录。
183
183
 
184
- ## 子代理模型授权(0.2.15 起)
184
+ ## 子代理模型授权(0.2.15 起,0.3.2 起强制指定模型)
185
185
 
186
186
  DSH 设置页的「Subagent」卡片会把勾选的模型写成会话级的允许列表(会话日志事件 `subagent/model-selection-policy`)。DSH 内置委派工具只拒绝**模型显式填写**且不在列表内的路由;调用里不写 `provider`/`model` 时,子代理会继承父级模型,于是白名单之外的主模型(例如 `deepseek-official/deepseek-flash`)仍会被子代理使用。
187
187
 
188
- 插件在 Host 工具注册表上补一个单调守卫(`ctx.tools.guard`),在委派执行前判定生效路由:
188
+ 插件在 Host 工具注册表上补一个单调守卫(`ctx.tools.guard`),在委派执行前要求**每次委派都必须写明路由**:
189
189
 
190
- - 调用会话(或最近的、记录了策略的祖先会话)带有允许列表时,生效路由必须落在列表内;
191
- - 不写路由的委派按“继承父级模型”判定,因此父级模型不在列表内时会被拒绝;
192
- - 拒绝结果里会列出全部已授权路由(例如 `antigravity/gemini-3.8-flash`),模型照此重试即可;`list_subagent_models` 仍只展示已授权路由;
193
- - 未记录允许列表的会话(例如恢复的旧会话、未启用该设置的会话)保持 DSH 原有行为。
190
+ - 调用会话(或最近的、记录了策略的祖先会话)带有允许列表时,`provider` 与 `model` 必须成对给出,且该路由必须落在列表内——缺省不再回退到配置默认值或父级模型;
191
+ - 只写一半(例如只给 `model`)同样被拒绝,拒绝理由会指出缺的是哪一个字段;
192
+ - **子代理永远和主代理同一个模型**的历史行为由此消失:模型必须先用 `list_subagent_models` 查出已授权路由,再按拒绝理由里列出的路由(例如 `antigravity/gemini-3.8-flash`)重试;该工具也只展示已授权路由;
193
+ - 未记录允许列表的会话(例如恢复的旧会话、未启用该设置的会话)保持 DSH 原有行为(继承父级模型)。
194
+
195
+ 守卫装在 DSH 工具注册表的调度入口上,因此 **`run_code`(代码模式 / PTC)里通过 SDK 调用的 `tools["subagent"]` 同样被拦截**:该子调用走的是与直接调用相同的 `prepare → guard → dispatch` 流水线,拒绝理由以 `ToolCallError` 抛回程序。也就是说模型无法靠把委派写进代码里绕过白名单(`test/subagent-model-authorization-ptc.test.ts` 用真实 `ToolRuntime` + 假 code runtime 验证了放行、缺省拒绝与越权拒绝三条路径)。
194
196
 
195
197
  配置项(插件行 `config`,全部可省略):
196
198
 
@@ -202,6 +204,8 @@ DSH 设置页的「Subagent」卡片会把勾选的模型写成会话级的允
202
204
 
203
205
  改动只在设置卡片里保存过的勾选生效:设置改动只影响之后新建的会话(DSH 的会话快照语义),已运行的会话继续使用它自己记录的那份列表。
204
206
 
207
+ > 升级提示:从 0.3.2 起,带有允许列表的会话里**任何**未写明 `provider` + `model` 的委派都会被拒绝(此前只有写明且不在列表内的路由会被拒绝)。这是有意的行为变更——它消除了「子代理悄悄继承主代理模型」的漏洞;把 `subagentModelAuthorization` 设为 `false` 可回到 DSH 原始行为。
208
+
205
209
  ### Kimi Code
206
210
 
207
211
  1. 重启 `dsh web`;
package/lib/client.js CHANGED
@@ -2813,10 +2813,11 @@ window.__ModuleLoader__.load({
2813
2813
  unselectAll: "全不选",
2814
2814
  wireAnthropic: "Anthropic",
2815
2815
  wireOpenai: "OpenAI",
2816
- capVideo: "视频",
2816
+ capVideo: "视频*",
2817
2817
  capDynamicTools: "动态工具",
2818
2818
  capImageOnly: "仅图片",
2819
2819
  capNone: "—",
2820
+ capVideoFootnote: "* 视频输入需先由 kimi_attach_video 工具挂载(本地绝对路径或 http(s) 链接);直接粘贴的上传路径仍只支持图片。",
2820
2821
  capabilitiesTitle: "模型能力",
2821
2822
  capColMedia: "多模态",
2822
2823
  capColTools: "动态工具",
@@ -2923,10 +2924,11 @@ window.__ModuleLoader__.load({
2923
2924
  unselectAll: "Deselect all",
2924
2925
  wireAnthropic: "Anthropic",
2925
2926
  wireOpenai: "OpenAI",
2926
- capVideo: "Video",
2927
+ capVideo: "Video*",
2927
2928
  capDynamicTools: "Dynamic tools",
2928
2929
  capImageOnly: "Images",
2929
2930
  capNone: "—",
2931
+ capVideoFootnote: "* Video input is attached with the kimi_attach_video tool (an absolute local path or an http(s) URL); the paste/upload path still accepts images only.",
2930
2932
  capabilitiesTitle: "Capabilities",
2931
2933
  capColMedia: "Multimodal",
2932
2934
  capColTools: "Dynamic tools",
@@ -2999,9 +3001,9 @@ window.__ModuleLoader__.load({
2999
3001
  */
3000
3002
  const t = zh$1;
3001
3003
  function KimiModelCapabilities({ models }) {
3002
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3004
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3003
3005
  className: "dsha-cap-table",
3004
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
3006
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
3005
3007
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t.capabilitiesTitle }),
3006
3008
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t.capColMedia }),
3007
3009
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t.capColTools }),
@@ -3033,7 +3035,10 @@ window.__ModuleLoader__.load({
3033
3035
  children: model.description ?? ""
3034
3036
  })
3035
3037
  ] }, model.id);
3036
- }) })] })
3038
+ }) })] }), models.some((model) => model.supportsVideo) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3039
+ className: "dsha-muted dsha-cap-footnote",
3040
+ children: t.capVideoFootnote
3041
+ })]
3037
3042
  });
3038
3043
  }
3039
3044
  //#endregion
@@ -4723,6 +4728,7 @@ window.__ModuleLoader__.load({
4723
4728
  .dsha-cap-on{color:var(--dsw-alias-label-primary);font-weight:500}
4724
4729
  .dsha-cap-off{color:var(--dsw-alias-label-tertiary)}
4725
4730
  .dsha-cap-notes{line-height:1.5;min-width:200px}
4731
+ .dsha-cap-footnote{font-size:11px;line-height:1.55;margin:8px 0 0}
4726
4732
  `;
4727
4733
  /** Kimi-specific rules layered on top of the shared control styles. */
4728
4734
  function installKimiCodeStyles() {