@inventec/dsh-copilot-auth 1.0.0 → 1.0.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 iasi
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iasi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,114 +1,120 @@
1
- # @inventec/dsh-copilot-auth
2
-
3
- 为 DSH(DeepSeek Harness)内置的 GitHub Copilot LLM provider 补上 Web 端设备码(device flow)登录/注销入口,并预置一条开箱即用的 GitHub Copilot 提供方路由。
4
-
5
- ## 这是什么
6
-
7
- 用公司分配的 GitHub 账号(带 Copilot 订阅)登录 DSH 的方式,和其他 Copilot 客户端一致:**网页 + 设备码**,全程不填 API Token。
8
-
9
- 本插件**复用 DSH 内置通道**(pi-ai 的 github-copilot provider),不实现任何 GitHub 协议代码——token 自动轮换、模型发现、协议适配全部由内置实现负责。插件本身只做三件事:挂载 DSH 内置但默认未启用的授权服务、提供「登录/注销」设置页、预置 `GitHub Copilot` 路由。
10
-
11
- ## 特性
12
-
13
- - 🔐 **设备码登录/注销**:网页 + user code,免 API Token;SSO 组织授权友好
14
- - 🧩 **预置提供方路由**:安装即出现在 Models 页,无需手动配置
15
- - 📦 **模型目录自动同步**:登录成功或插件启动时,自动把账号全部可用模型写入模型目录,开箱即用
16
- - 🌐 **中/英双语界面**:跟随 DSH 语言设置自动切换
17
- - 🔑 **凭据安全托管**:存入 DSH 内置凭据库(文件强制 0600 权限),Copilot 临时 token 到期自动刷新
18
- - 🧪 **测试与 CI**:13 条单元测试;GitHub Actions 构建测试 + tag 触发自动发布(provenance)
19
-
20
- ## 前置要求
21
-
22
- - DSH `0.1.2-rc.1`(实测版本)
23
- - 有效的 GitHub Copilot 订阅(公司分配的 github.com 组织账号,SSO 登录)
24
- - `pnpm` 可用(`dsh plugin` 是 pnpm 转发器)
25
-
26
- ## 安装
27
-
28
- ```bash
29
- dsh plugin --profile web add @inventec/dsh-copilot-auth
30
- ```
31
-
32
- 然后**重启 dsh web 并刷新浏览器**。
33
-
34
- ### 故障排查
35
-
36
- - 企业镜像/私有源环境下 pnpm 解析 peer 失败:`export npm_config_auto_install_peers=false` 后重试(profile 自带的 pnpm workspace 默认已关闭 peer 自动安装)。
37
- - registry 的 `latest` dist-tag 可能落后于 `next`:显式按版本号安装/排查时勿被 `latest` 误导。
38
-
39
- ## 登录
40
-
41
- 1. 打开设置,进入侧栏「**GHC设置**」(英文界面为 *GHC Settings*,位于 Models 之后)
42
- 2. 点「登录」,页面显示一串**代码**(user code,可一键复制)与 `https://github.com/login/device` 链接
43
- 3. 新标签打开链接,用公司 GitHub 账号输入代码
44
- 4. **SSO 用户必须对组织点 Authorize**(授权页会出现该步骤,跳过则登录不生效)
45
- 5. 回到设置页看到「已登录」即成功;模型目录此时已自动同步
46
-
47
- > 设备码有时效(约 15 分钟),请拿到代码后尽快完成授权。若超时,页面会显示失败原因,重新点「登录」即可。
48
-
49
- ## 使用
50
-
51
- - Models 页选择 `GitHub Copilot` 路由,模型目录已在登录/启动时自动同步账号全部可用模型,无需手动「添加模型」
52
- - **配额说明**:base 模型(GPT-4o/4.1 一类)不耗 premium requests;premium 模型(Claude、Gemini、o 系列等)每次调用消耗月度配额。日常建议 base 档,premium 模型按需手动选
53
-
54
- ## 注销与卸载
55
-
56
- - 注销:设置页「注销」按钮(清除本机凭据记录)
57
- - 卸载:`dsh plugin --profile web remove @inventec/dsh-copilot-auth` 后重启 dsh web
58
-
59
- ## 工作原理
60
-
61
- - 通过 `cordis.patch.yml` 三段生效:
62
- 1. 挂载 `@deepseek-ai/dsh-authorization` 服务(DSH 内置 bundle 未挂载,不挂则内置登录流不存在)
63
- 2. 注册本插件(host 侧在 webserver 上开 4 条本地路由:`/copilot-auth/start|state|status|logout`,跨站 Origin 拒绝)
64
- 3. 以 settings base 层预置 `github-copilot` 路由(用户 `settings.yaml` 可逐字段覆盖)
65
- - 登录走 GitHub 设备码流;凭据存 `~/.dsh/.credentials.yaml`(强制 600 权限)的 `llm-pi-ai/github-copilot` 记录,Copilot 临时 token 到期自动刷新
66
- - **模型目录自动同步**:登录成功或插件启动时,取「账号可用模型 ∩ pi-ai 内置目录」写入该路由的模型目录,同步失败经 `/copilot-auth/status` 的 `syncError` 字段暴露
67
-
68
- ## 开发
69
-
70
- ```bash
71
- npm install
72
- npm test # node:test:patch 结构 + host 行为共 13 条
73
- npm run build # esbuild 打包 client 到 lib/client.js(__ModuleLoader__ 信封)
74
- npm pack --dry-run
75
- ```
76
-
77
- | 路径 | 职责 |
78
- |---|---|
79
- | `cordis.patch.yml` | 三段 patch(authorization 挂载 / 本插件 entry / 预置路由) |
80
- | `src/host.mjs` · `src/shared.mjs` | host 半区:登录状态机、4 条本地路由、模型目录同步 |
81
- | `src/client.jsx` | client 半区:settings.section 插槽 + 设备码交互 + 双语文案 |
82
- | `scripts/build-client.mjs` | client 打包(`__ModuleLoader__` CJS 工厂信封) |
83
- | `lib/client.js` | 构建产物(入库,使 git 安装免构建) |
84
-
85
- 从源码安装:`dsh plugin --profile web add /path/to/dsh-copilot-auth`
86
-
87
- ## 风险与合规
88
-
89
- - 通道为社区通用路线(基于 pi-ai 内部实现),**非 GitHub 官方支持 API**,可能随服务端变更失效
90
- - GitHub AUP 禁止批量自动化滥用;**公司账号请正常强度使用**
91
-
92
- ## 已知边界
93
-
94
- - Models 页的凭据状态圆点只反映 API Key 引用,OAuth 登录成功后圆点仍可能显示未配置(外观问题,功能不受影响)
95
- - 自行在 `cordis.patch.yml` patch `llm-pi-ai` 整段 config 会覆盖预置路由
96
- - `settings.yaml` 的 `llm-pi-ai:` 节只能稀疏覆盖字段,无法删除预置路由本身(移除须卸载本插件)
97
- - 路由前缀 `/copilot-auth` 两侧硬编码,不可配置
98
- - DSH rc 版本耦合:实测 `0.1.2-rc.1`,peer 仅 `@deepseek-ai/cordis@^4.0.2`
99
- - 每次登录成功或插件启动,会用账号最新发现的可用模型**全量覆盖** GitHub Copilot 路由的模型目录——对目录的手工定制(如模型显示名)会在下次同步时被重置为裸模型 id
100
- - 模型目录只写入 pi-ai 内置目录已描述的模型:目录快照外的新模型暂不写入(catalog 路由的校验要求模型协议可解析),pi-ai 升级目录后会随同步自动出现
101
-
102
- ## 维护者发布
103
-
104
- 本项目使用 npm **Trusted Publishing(GitHub Actions OIDC)** 发布,不在仓库或 GitHub Secrets 中保存 `NPM_TOKEN`。
105
-
106
- - 发布 workflow:`.github/workflows/release.yml`
107
- - 触发方式:推送 `v*` tag,或在 GitHub Actions 手动触发
108
- - npm Trusted Publisher 配置:GitHub Actions;owner `iasiv5`;repository `dsh-copilot-auth`;workflow filename `release.yml`;environment 留空;允许 `npm publish`
109
- - workflow 使用 GitHub-hosted runner、`id-token: write`,npm CLI 自动使用 OIDC 并生成 provenance
110
- - npm Trusted Publisher 是按 package 配置的。首次发布前如果 package 尚不存在,需要先完成 npm 要求的一次性 bootstrap,再在 package settings 中配置 Trusted Publisher;bootstrap 不应通过长期 GitHub secret 实现。详见 [npm Trusted Publishing 文档](https://docs.npmjs.com/trusted-publishers)
111
-
112
- ## License
113
-
114
- MIT
1
+ # @inventec/dsh-copilot-auth
2
+
3
+ [![Release](https://img.shields.io/github/v/release/iasiv5/dsh-copilot-auth?label=Release&sort=semver)](https://github.com/iasiv5/dsh-copilot-auth/releases)
4
+ [![npm](https://img.shields.io/npm/v/%40inventec%2Fdsh-copilot-auth?label=npm)](https://www.npmjs.com/package/@inventec/dsh-copilot-auth)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/iasiv5/dsh-copilot-auth/ci.yml?branch=main&label=CI)](https://github.com/iasiv5/dsh-copilot-auth/actions/workflows/ci.yml)
6
+ [![License](https://img.shields.io/github/license/iasiv5/dsh-copilot-auth?label=License)](https://github.com/iasiv5/dsh-copilot-auth/blob/main/LICENSE)
7
+ [![DSH Web](https://img.shields.io/badge/DSH%20Web-0.1.2--rc.1%20verified-2563eb)](#前置要求)
8
+
9
+ 为 DSH(DeepSeek Harness)内置的 GitHub Copilot LLM provider 补上 Web 端设备码(device flow)登录/注销入口,并预置一条开箱即用的 GitHub Copilot 提供方路由。
10
+
11
+ ## 这是什么
12
+
13
+ 用公司分配的 GitHub 账号(带 Copilot 订阅)登录 DSH 的方式,和其他 Copilot 客户端一致:**网页 + 设备码**,全程不填 API Token。
14
+
15
+ 本插件**复用 DSH 内置通道**(pi-ai 的 github-copilot provider),不实现任何 GitHub 协议代码——token 自动轮换、模型发现、协议适配全部由内置实现负责。插件本身只做三件事:挂载 DSH 内置但默认未启用的授权服务、提供「登录/注销」设置页、预置 `GitHub Copilot` 路由。
16
+
17
+ ## 特性
18
+
19
+ - 🔐 **设备码登录/注销**:网页 + user code,免 API Token;SSO 组织授权友好
20
+ - 🧩 **预置提供方路由**:安装即出现在 Models 页,无需手动配置
21
+ - 📦 **模型目录兜底填充**:登录成功时,若 GitHub Copilot 路由还没有模型目录,自动填入账号全部可用模型;你已精简/定制过的目录**永不覆盖**(重启、重新登录均不重置)
22
+ - 🌐 **中/英双语界面**:跟随 DSH 语言设置自动切换
23
+ - 🔑 **凭据安全托管**:存入 DSH 内置凭据库(文件强制 0600 权限),Copilot 临时 token 到期自动刷新
24
+ - 🧪 **测试与 CI**:15 条单元测试;GitHub Actions 构建测试 + tag 触发自动发布(provenance)
25
+
26
+ ## 前置要求
27
+
28
+ - DSH `0.1.2-rc.1`(实测版本)
29
+ - 有效的 GitHub Copilot 订阅(公司分配的 github.com 组织账号,SSO 登录)
30
+ - `pnpm` 可用(`dsh plugin` 是 pnpm 转发器)
31
+
32
+ ## 安装
33
+
34
+ ```bash
35
+ dsh plugin --profile web add @inventec/dsh-copilot-auth
36
+ ```
37
+
38
+ 然后**重启 dsh web 并刷新浏览器**。
39
+
40
+ ### 故障排查
41
+
42
+ - 企业镜像/私有源环境下 pnpm 解析 peer 失败:`export npm_config_auto_install_peers=false` 后重试(profile 自带的 pnpm workspace 默认已关闭 peer 自动安装)。
43
+ - registry 的 `latest` dist-tag 可能落后于 `next`:显式按版本号安装/排查时勿被 `latest` 误导。
44
+
45
+ ## 登录
46
+
47
+ 1. 打开设置,进入侧栏「**GHC设置**」(英文界面为 *GHC Settings*,位于 Models 之后)
48
+ 2. 点「登录」,页面显示一串**代码**(user code,可一键复制)与 `https://github.com/login/device` 链接
49
+ 3. 新标签打开链接,用公司 GitHub 账号输入代码
50
+ 4. **SSO 用户必须对组织点 Authorize**(授权页会出现该步骤,跳过则登录不生效)
51
+ 5. 回到设置页看到「已登录」即成功;首次登录会自动填充模型目录(已定制过的目录不会被覆盖)
52
+
53
+ > 设备码有时效(约 15 分钟),请拿到代码后尽快完成授权。若超时,页面会显示失败原因,重新点「登录」即可。
54
+
55
+ ## 使用
56
+
57
+ - Models 页选择 `GitHub Copilot` 路由,模型目录已在首次登录时自动填充账号全部可用模型,无需手动「添加模型」;之后可自由增删精简,重启 / 重新登录都不会重置你的列表
58
+ - **配额说明**:base 模型(GPT-4o/4.1 一类)不耗 premium requests;premium 模型(Claude、Gemini、o 系列等)每次调用消耗月度配额。日常建议 base 档,premium 模型按需手动选
59
+
60
+ ## 注销与卸载
61
+
62
+ - 注销:设置页「注销」按钮(清除本机凭据记录)
63
+ - 卸载:`dsh plugin --profile web remove @inventec/dsh-copilot-auth` 后重启 dsh web
64
+
65
+ ## 工作原理
66
+
67
+ - 通过 `cordis.patch.yml` 三段生效:
68
+ 1. 挂载 `@deepseek-ai/dsh-authorization` 服务(DSH 内置 bundle 未挂载,不挂则内置登录流不存在)
69
+ 2. 注册本插件(host 侧在 webserver 上开 4 条本地路由:`/copilot-auth/start|state|status|logout`,跨站 Origin 拒绝)
70
+ 3. 以 settings base 层预置 `github-copilot` 路由(用户 `settings.yaml` 可逐字段覆盖)
71
+ - 登录走 GitHub 设备码流;凭据存 `~/.dsh/.credentials.yaml`(强制 600 权限)的 `llm-pi-ai/github-copilot` 记录,Copilot 临时 token 到期自动刷新
72
+ - **模型目录兜底填充**:登录成功时,取「账号可用模型 ∩ pi-ai 内置目录」写入该路由的模型目录——仅当该路由尚未配置 `models` 且无 `modelOverrides` 时写入;目录已存在(含空列表)一律让路,插件启动/重启也绝不触碰用户 settings。同步失败经 `/copilot-auth/status` 的 `syncError` 字段暴露
73
+
74
+ ## 开发
75
+
76
+ ```bash
77
+ npm install
78
+ npm test # node:test:patch 结构 + host 行为共 15 条
79
+ npm run build # esbuild 打包 client 到 lib/client.js(__ModuleLoader__ 信封)
80
+ npm pack --dry-run
81
+ ```
82
+
83
+ | 路径 | 职责 |
84
+ |---|---|
85
+ | `cordis.patch.yml` | 三段 patch(authorization 挂载 / 本插件 entry / 预置路由) |
86
+ | `src/host.mjs` · `src/shared.mjs` | host 半区:登录状态机、4 条本地路由、模型目录同步 |
87
+ | `src/client.jsx` | client 半区:settings.section 插槽 + 设备码交互 + 双语文案 |
88
+ | `scripts/build-client.mjs` | client 打包(`__ModuleLoader__` CJS 工厂信封) |
89
+ | `lib/client.js` | 构建产物(入库,使 git 安装免构建) |
90
+
91
+ 从源码安装:`dsh plugin --profile web add /path/to/dsh-copilot-auth`
92
+
93
+ ## 风险与合规
94
+
95
+ - 通道为社区通用路线(基于 pi-ai 内部实现),**非 GitHub 官方支持 API**,可能随服务端变更失效
96
+ - GitHub AUP 禁止批量自动化滥用;**公司账号请正常强度使用**
97
+
98
+ ## 已知边界
99
+
100
+ - Models 页的凭据状态圆点只反映 API Key 引用,OAuth 登录成功后圆点仍可能显示未配置(外观问题,功能不受影响)
101
+ - 自行在 `cordis.patch.yml` patch `llm-pi-ai` 整段 config 会覆盖预置路由
102
+ - `settings.yaml` 的 `llm-pi-ai:` 节只能稀疏覆盖字段,无法删除预置路由本身(移除须卸载本插件)
103
+ - 路由前缀 `/copilot-auth` 两侧硬编码,不可配置
104
+ - DSH rc 版本耦合:实测 `0.1.2-rc.1`,peer 仅 `@deepseek-ai/cordis@^4.0.2`
105
+ - 模型目录只在「尚不存在」时由登录成功兜底填充一次,填充后归用户所有:账号新增的模型不会自动出现,需在 Models 页手动添加,或删掉 `settings.yaml` 里该路由的 `models` 列表后**重新登录**重新填充全量(插件启动不触发填充)
106
+ - 模型目录只写入 pi-ai 内置目录已描述的模型:目录快照外的新模型暂不写入(catalog 路由的校验要求模型协议可解析),pi-ai 升级目录后重新登录即可纳入
107
+
108
+ ## 维护者发布
109
+
110
+ 本项目使用 npm **Trusted Publishing(GitHub Actions OIDC)** 发布,不在仓库或 GitHub Secrets 中保存 `NPM_TOKEN`。
111
+
112
+ - 发布 workflow:`.github/workflows/release.yml`
113
+ - 触发方式:推送 `v*` tag,或在 GitHub Actions 手动触发
114
+ - npm Trusted Publisher 配置:GitHub Actions;owner `iasiv5`;repository `dsh-copilot-auth`;workflow filename `release.yml`;environment 留空;允许 `npm publish`
115
+ - workflow 使用 GitHub-hosted runner、`id-token: write`,npm CLI 自动使用 OIDC 并生成 provenance
116
+ - npm Trusted Publisher 是按 package 配置的。首次发布前如果 package 尚不存在,需要先完成 npm 要求的一次性 bootstrap,再在 package settings 中配置 Trusted Publisher;bootstrap 不应通过长期 GitHub secret 实现。详见 [npm Trusted Publishing 文档](https://docs.npmjs.com/trusted-publishers)
117
+
118
+ ## License
119
+
120
+ MIT
package/cordis.patch.yml CHANGED
@@ -1,20 +1,20 @@
1
- # 1) 前置:0.1.2-rc.1 的全部 6 个内置 bundle 均未挂载 authorization 服务;
2
- # dsh-llm-pi-ai 的登录流注册是被动 ctx.inject(lib/index.js L2501),不挂此行则 flow 不存在。
3
- - insert:
4
- - id: copilot-authorization
5
- name: '@deepseek-ai/dsh-authorization'
6
- # 2) 本插件 host 半区(web 树有 webServer 服务才会激活)
7
- # 路由前缀固定 /copilot-auth、不做配置项:client 侧 fetch 硬编码同一路径,
8
- # 配置一旦漂移 client 即 404(见附录 B)
9
- - id: copilot-auth
10
- name: '@inventec/dsh-copilot-auth'
11
- # 3) 预置 GitHub Copilot 路由:entry config 作为 settings base 层,
12
- # 用户 settings.yaml 的 llm-pi-ai: 节逐字段递归覆盖其上。
13
- # 不写 apiKeyEnv → 认证走 OAuth 凭据记录(llm-pi-ai/github-copilot)。
14
- # 带 name 做 mismatch 防御:未来 base 若改名,patch 警告而非错挂他行。
15
- - id: llm-pi-ai
16
- name: '@deepseek-ai/dsh-llm-pi-ai'
17
- config:
18
- providers:
19
- github-copilot:
20
- displayName: GitHub Copilot
1
+ # 1) 前置:0.1.2-rc.1 的全部 6 个内置 bundle 均未挂载 authorization 服务;
2
+ # dsh-llm-pi-ai 的登录流注册是被动 ctx.inject(lib/index.js L2501),不挂此行则 flow 不存在。
3
+ - insert:
4
+ - id: copilot-authorization
5
+ name: '@deepseek-ai/dsh-authorization'
6
+ # 2) 本插件 host 半区(web 树有 webServer 服务才会激活)
7
+ # 路由前缀固定 /copilot-auth、不做配置项:client 侧 fetch 硬编码同一路径,
8
+ # 配置一旦漂移 client 即 404(见附录 B)
9
+ - id: copilot-auth
10
+ name: '@inventec/dsh-copilot-auth'
11
+ # 3) 预置 GitHub Copilot 路由:entry config 作为 settings base 层,
12
+ # 用户 settings.yaml 的 llm-pi-ai: 节逐字段递归覆盖其上。
13
+ # 不写 apiKeyEnv → 认证走 OAuth 凭据记录(llm-pi-ai/github-copilot)。
14
+ # 带 name 做 mismatch 防御:未来 base 若改名,patch 警告而非错挂他行。
15
+ - id: llm-pi-ai
16
+ name: '@deepseek-ai/dsh-llm-pi-ai'
17
+ config:
18
+ providers:
19
+ github-copilot:
20
+ displayName: GitHub Copilot
package/package.json CHANGED
@@ -1,51 +1,55 @@
1
- {
2
- "name": "@inventec/dsh-copilot-auth",
3
- "version": "1.0.0",
4
- "description": "Device-code sign-in UI and preconfigured route for the built-in GitHub Copilot provider of DeepSeek Harness (DSH)",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "iasi",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/iasiv5/dsh-copilot-auth.git"
11
- },
12
- "main": "./src/host.mjs",
13
- "exports": {
14
- ".": "./src/host.mjs",
15
- "./client": "./lib/client.js",
16
- "./package.json": "./package.json"
17
- },
18
- "files": [
19
- "src/host.mjs",
20
- "src/shared.mjs",
21
- "lib/client.js",
22
- "cordis.patch.yml",
23
- "README.md",
24
- "LICENSE"
25
- ],
26
- "scripts": {
27
- "build": "node scripts/build-client.mjs",
28
- "test": "node --test",
29
- "prepack": "npm run build"
30
- },
31
- "engines": { "node": ">=20" },
32
- "peerDependencies": {
33
- "@deepseek-ai/cordis": "^4.0.2"
34
- },
35
- "devDependencies": {
36
- "esbuild": "^0.24.0",
37
- "react": "^18.2.0",
38
- "yaml": "^2.5.0"
39
- },
40
- "dsh": {
41
- "bundle": { "patch": "./cordis.patch.yml" },
42
- "client": {
43
- "platform": "web",
44
- "inject": [
45
- "@deepseek-ai/dsh-client-ui-settings",
46
- "@deepseek-ai/dsh-client-locale",
47
- "@deepseek-ai/dsh-api-remotes"
48
- ]
49
- }
50
- }
51
- }
1
+ {
2
+ "name": "@inventec/dsh-copilot-auth",
3
+ "version": "1.0.2",
4
+ "description": "Device-code sign-in UI and preconfigured route for the built-in GitHub Copilot provider of DeepSeek Harness (DSH)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "iasi",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/iasiv5/dsh-copilot-auth.git"
11
+ },
12
+ "main": "./src/host.mjs",
13
+ "exports": {
14
+ ".": "./src/host.mjs",
15
+ "./client": "./lib/client.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "src/host.mjs",
20
+ "src/shared.mjs",
21
+ "lib/client.js",
22
+ "cordis.patch.yml",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "build": "node scripts/build-client.mjs",
28
+ "test": "node --test",
29
+ "prepack": "npm run build"
30
+ },
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "peerDependencies": {
35
+ "@deepseek-ai/cordis": "^4.0.2"
36
+ },
37
+ "devDependencies": {
38
+ "esbuild": "^0.24.0",
39
+ "react": "^18.2.0",
40
+ "yaml": "^2.5.0"
41
+ },
42
+ "dsh": {
43
+ "bundle": {
44
+ "patch": "./cordis.patch.yml"
45
+ },
46
+ "client": {
47
+ "platform": "web",
48
+ "inject": [
49
+ "@deepseek-ai/dsh-client-ui-settings",
50
+ "@deepseek-ai/dsh-client-locale",
51
+ "@deepseek-ai/dsh-api-remotes"
52
+ ]
53
+ }
54
+ }
55
+ }
package/src/host.mjs CHANGED
@@ -1,185 +1,207 @@
1
- // host 半区:cordis 插件。注册 4 条 exact 路由,把内置 github-copilot 的
2
- // OAuth 设备码流(ctx.authorization)暴露给 Web client。零 GitHub 协议代码。
3
- import { existsSync, readFileSync, realpathSync } from "node:fs";
4
- import { dirname, join } from "node:path";
5
- import { CREDENTIAL_KEY, emptyState, routes } from "./shared.mjs";
6
-
7
- export const name = "copilot-auth";
8
- export const inject = ["webServer", "authorization", "credentials", "settings", "llm"];
9
-
10
- // 读取 pi-ai 内置目录的 github-copilot 模型 id → 协议映射。数据文件位于 dsh
11
- // 安装树内的 @earendil-works/pi-ai;从进程入口(dsh 可执行文件)逐级向上定位,
12
- // 只读不改。定位/解析失败返回 null(调用方回退 listModels 交集,见下)。
13
- function readCatalogModelIds() {
14
- try {
15
- let dir = dirname(realpathSync(process.argv?.[1] ?? ""));
16
- for (let depth = 0; depth < 8; depth++) {
17
- const dataFile = join(dir, "node_modules", "@earendil-works", "pi-ai", "dist", "providers", "data", "github-copilot.json");
18
- if (existsSync(dataFile)) {
19
- const data = JSON.parse(readFileSync(dataFile, "utf8"));
20
- const byId = {};
21
- for (const [api, section] of Object.entries(data)) {
22
- for (const id of Object.keys(section ?? {})) byId[id] ??= api;
23
- }
24
- return byId;
25
- }
26
- const parent = dirname(dir);
27
- if (parent === dir) return null;
28
- dir = parent;
29
- }
30
- } catch { /* 安装树结构变化时回退 */ }
31
- return null;
32
- }
33
-
34
- // 登录成功(或挂载时已登录)后,把账号可用模型写入用户 settings 的模型目录
35
- // (turnkey,用户新增需求 2026-09-04)。
36
- // 可写集合取交集:凭据 payload.availableModelIds(账号可用)∩ 内置目录已描述。
37
- // 目录快照外的 id 必须排除——catalog 路由校验要求模型的 wire 协议可解析
38
- // (api = 路由设置 ?? 目录条目 ?? 路由共享协议),目录外 id 三者皆空会被整体
39
- // 拒绝;pi-ai 目录更新后它们会自然进入可写集合。
40
- // 注意:每次登录/挂载会用最新列表覆盖该目录,手工定制会被重置。
41
- async function syncAvailableModels(ctx) {
42
- const record = await ctx.credentials.readRecord(CREDENTIAL_KEY);
43
- const available = record?.payload?.availableModelIds;
44
- if (!Array.isArray(available) || available.length === 0) return;
45
- const catalog = readCatalogModelIds();
46
- let ids;
47
- if (catalog) {
48
- ids = available.filter((id) => catalog[id] !== undefined);
49
- } else {
50
- // 回退:listModels 反映当前已配置列表,存在「已配置→只见已配置」自锁,
51
- // 仅作目录定位失败时的降级。
52
- const served = new Set((await ctx.llm.listModels("github-copilot")).map((m) => m?.id).filter(Boolean));
53
- ids = available.filter((id) => served.has(id));
54
- }
55
- if (ids.length === 0) return;
56
- await ctx.settings.mutate("llm-pi-ai", [
57
- { op: "set", path: ["providers", "github-copilot", "models"], value: ids.map((id) => ({ id })) },
58
- ]);
59
- }
60
-
61
- function sameOrigin(req) {
62
- const origin = req.headers?.origin;
63
- if (origin === undefined || origin === null || origin === "") return true;
64
- try {
65
- return new URL(origin).host === req.headers.host;
66
- } catch {
67
- return false;
68
- }
69
- }
70
-
71
- function json(res, code, payload) {
72
- res.writeHead(code, { "content-type": "application/json" });
73
- res.end(JSON.stringify(payload));
74
- }
75
-
76
- function guard(req, res, method) {
77
- if (!sameOrigin(req)) {
78
- json(res, 403, { ok: false, error: "forbidden origin" });
79
- return false;
80
- }
81
- if (req.method !== method) {
82
- json(res, 405, { ok: false, error: "method not allowed" });
83
- return false;
84
- }
85
- return true;
86
- }
87
-
88
- export function apply(ctx) {
89
- const r = routes();
90
- let attempt = emptyState();
91
- let lastSyncError;
92
-
93
- // 挂载时若已登录,同步一次模型目录(凭据持久化在本地,模型列表随账号刷新)。
94
- // 失败不静默:错误经 /status 的 syncError 字段暴露,便于诊断。
95
- const sync = () => syncAvailableModels(ctx).then(() => { lastSyncError = undefined; }).catch((err) => {
96
- lastSyncError = String(err?.message ?? err);
97
- ctx.logger?.warn?.("copilot-auth: model sync failed: %s", lastSyncError);
98
- });
99
- void sync();
100
-
101
- ctx.webServer.register({
102
- kind: "exact",
103
- path: r.start,
104
- handler: (req, res) => {
105
- if (!guard(req, res, "POST")) return;
106
- if (attempt.status === "running") {
107
- json(res, 409, { ok: false, error: "already running" });
108
- return;
109
- }
110
- attempt = emptyState();
111
- attempt.status = "running";
112
- const interaction = {
113
- notify: (notice) => {
114
- attempt.notices.push(notice);
115
- },
116
- prompt: (p) => {
117
- // 企业域名提问答空串(公司是普通 github.com 组织账号);
118
- // 其余任何 prompt 都是未预期的,拒绝并使 attempt 失败。
119
- if (p && typeof p.message === "string" && p.message.includes("Enterprise")) {
120
- return Promise.resolve("");
121
- }
122
- return Promise.reject(new Error("unexpected prompt: " + (p?.message ?? String(p))));
123
- },
124
- };
125
- // 响应先行:begin 以后台任务执行,handler 返回路径不得 await begin。
126
- void ctx.authorization
127
- .begin({ key: CREDENTIAL_KEY, method: "oauth", interaction })
128
- .then((outcome) => {
129
- // AuthorizationOutcome.status: 'authorized' | 'cancelled'(types.d.ts L68-71)
130
- if (outcome && outcome.status === "authorized") {
131
- attempt.status = "authorized";
132
- void sync();
133
- } else {
134
- attempt.status = "failed";
135
- attempt.error = "登录已取消";
136
- }
137
- })
138
- .catch((err) => {
139
- attempt.status = "failed";
140
- attempt.error = String(err?.message ?? err);
141
- });
142
- json(res, 202, { ok: true });
143
- },
144
- });
145
-
146
- ctx.webServer.register({
147
- kind: "exact",
148
- path: r.state,
149
- handler: (req, res) => {
150
- if (!guard(req, res, "GET")) return;
151
- json(res, 200, attempt);
152
- },
153
- });
154
-
155
- ctx.webServer.register({
156
- kind: "exact",
157
- path: r.status,
158
- handler: async (req, res) => {
159
- if (!guard(req, res, "GET")) return;
160
- // describeRecord 只回传 presence,不把含 token 的 GrantRecord 拉进内存。
161
- let configured = false;
162
- try {
163
- const info = await ctx.credentials.describeRecord(CREDENTIAL_KEY);
164
- configured = info?.configured === true;
165
- } catch {
166
- configured = false;
167
- }
168
- json(res, 200, { configured, syncError: lastSyncError });
169
- },
170
- });
171
-
172
- ctx.webServer.register({
173
- kind: "exact",
174
- path: r.logout,
175
- handler: async (req, res) => {
176
- if (!guard(req, res, "POST")) return;
177
- // 幂等契约:deleteRecord 对不存在记录是 no-op 且正常 resolve,
178
- // 真实删除与否由随后的 /status 反映。
179
- await ctx.credentials.deleteRecord(CREDENTIAL_KEY);
180
- json(res, 200, { ok: true });
181
- },
182
- });
183
- }
184
-
185
- export default { name, inject, apply };
1
+ // host 半区:cordis 插件。注册 4 条 exact 路由,把内置 github-copilot 的
2
+ // OAuth 设备码流(ctx.authorization)暴露给 Web client。零 GitHub 协议代码。
3
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { CREDENTIAL_KEY, emptyState, routes } from "./shared.mjs";
6
+
7
+ export const name = "copilot-auth";
8
+ export const inject = ["webServer", "authorization", "credentials", "settings", "llm"];
9
+
10
+ // 读取 pi-ai 内置目录的 github-copilot 模型 id → 协议映射。数据文件位于 dsh
11
+ // 安装树内的 @earendil-works/pi-ai;从进程入口(dsh 可执行文件)逐级向上定位,
12
+ // 只读不改。定位/解析失败返回 null(调用方回退 listModels 交集,见下)。
13
+ function readCatalogModelIds() {
14
+ try {
15
+ let dir = dirname(realpathSync(process.argv?.[1] ?? ""));
16
+ for (let depth = 0; depth < 8; depth++) {
17
+ const dataFile = join(dir, "node_modules", "@earendil-works", "pi-ai", "dist", "providers", "data", "github-copilot.json");
18
+ if (existsSync(dataFile)) {
19
+ const data = JSON.parse(readFileSync(dataFile, "utf8"));
20
+ const byId = {};
21
+ for (const [api, section] of Object.entries(data)) {
22
+ for (const id of Object.keys(section ?? {})) byId[id] ??= api;
23
+ }
24
+ return byId;
25
+ }
26
+ const parent = dirname(dir);
27
+ if (parent === dir) return null;
28
+ dir = parent;
29
+ }
30
+ } catch { /* 安装树结构变化时回退 */ }
31
+ return null;
32
+ }
33
+
34
+ // 登录成功后,把账号可用模型写入用户 settings 的模型目录
35
+ // (turnkey,用户新增需求 2026-09-04;目录保护,用户反馈 2026-09-05)。
36
+ // 可写集合取交集:凭据 payload.availableModelIds(账号可用)∩ 内置目录已描述。
37
+ // 目录快照外的 id 必须排除——catalog 路由校验要求模型的 wire 协议可解析
38
+ // (api = 路由设置 ?? 目录条目 ?? 路由共享协议),目录外 id 三者皆空会被整体
39
+ // 拒绝;pi-ai 目录更新后它们会自然进入可写集合。
40
+ // 目录保护(2026-09-05):该路由已配置模型目录(models 键存在,含空列表)或
41
+ // modelOverrides 时视作用户所有,同步直接让路——用户在 Models 页精简过的目录
42
+ // 不会被重置。只有目录尚不存在时才填充一次;想重置全量,删掉 settings.yaml
43
+ // 里该路由的 models 列表后重新登录即可。
44
+
45
+ // 读取解析后的 github-copilot 路由配置(base 层 + 用户层的最终值)。
46
+ // settings 未注入 / 未注册 / 读取失败一律按空处理——回退到可写入分支,
47
+ // 保持既有 turnkey 行为不变。
48
+ function readConfiguredRoute(ctx) {
49
+ try {
50
+ const section = ctx.settings?.get?.("llm-pi-ai");
51
+ return section?.providers?.["github-copilot"] ?? {};
52
+ } catch {
53
+ return {};
54
+ }
55
+ }
56
+
57
+ async function syncAvailableModels(ctx) {
58
+ const record = await ctx.credentials.readRecord(CREDENTIAL_KEY);
59
+ const available = record?.payload?.availableModelIds;
60
+ if (!Array.isArray(available) || available.length === 0) return;
61
+ const route = readConfiguredRoute(ctx);
62
+ const modelsConfigured = route.models !== undefined && route.models !== null;
63
+ const overridesConfigured = !!route.modelOverrides
64
+ && typeof route.modelOverrides === "object"
65
+ && Object.keys(route.modelOverrides).length > 0;
66
+ if (modelsConfigured || overridesConfigured) return;
67
+ const catalog = readCatalogModelIds();
68
+ let ids;
69
+ if (catalog) {
70
+ ids = available.filter((id) => catalog[id] !== undefined);
71
+ } else {
72
+ // 回退:listModels 反映当前已配置列表,存在「已配置→只见已配置」自锁,
73
+ // 仅作目录定位失败时的降级。
74
+ const served = new Set((await ctx.llm.listModels("github-copilot")).map((m) => m?.id).filter(Boolean));
75
+ ids = available.filter((id) => served.has(id));
76
+ }
77
+ if (ids.length === 0) return;
78
+ await ctx.settings.mutate("llm-pi-ai", [
79
+ { op: "set", path: ["providers", "github-copilot", "models"], value: ids.map((id) => ({ id })) },
80
+ ]);
81
+ }
82
+
83
+ function sameOrigin(req) {
84
+ const origin = req.headers?.origin;
85
+ if (origin === undefined || origin === null || origin === "") return true;
86
+ try {
87
+ return new URL(origin).host === req.headers.host;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ function json(res, code, payload) {
94
+ res.writeHead(code, { "content-type": "application/json" });
95
+ res.end(JSON.stringify(payload));
96
+ }
97
+
98
+ function guard(req, res, method) {
99
+ if (!sameOrigin(req)) {
100
+ json(res, 403, { ok: false, error: "forbidden origin" });
101
+ return false;
102
+ }
103
+ if (req.method !== method) {
104
+ json(res, 405, { ok: false, error: "method not allowed" });
105
+ return false;
106
+ }
107
+ return true;
108
+ }
109
+
110
+ export function apply(ctx) {
111
+ const r = routes();
112
+ let attempt = emptyState();
113
+ let lastSyncError;
114
+
115
+ // 模型目录同步只在登录成功后执行(2026-09-05 对齐结论:挂载不再同步)。
116
+ // 插件启动/重启绝不触碰用户 settings,用户精简过的目录不会被动重置。
117
+ // 失败不静默:错误经 /status 的 syncError 字段暴露,便于诊断。
118
+ const sync = () => syncAvailableModels(ctx).then(() => { lastSyncError = undefined; }).catch((err) => {
119
+ lastSyncError = String(err?.message ?? err);
120
+ ctx.logger?.warn?.("copilot-auth: model sync failed: %s", lastSyncError);
121
+ });
122
+
123
+ ctx.webServer.register({
124
+ kind: "exact",
125
+ path: r.start,
126
+ handler: (req, res) => {
127
+ if (!guard(req, res, "POST")) return;
128
+ if (attempt.status === "running") {
129
+ json(res, 409, { ok: false, error: "already running" });
130
+ return;
131
+ }
132
+ attempt = emptyState();
133
+ attempt.status = "running";
134
+ const interaction = {
135
+ notify: (notice) => {
136
+ attempt.notices.push(notice);
137
+ },
138
+ prompt: (p) => {
139
+ // 企业域名提问答空串(公司是普通 github.com 组织账号);
140
+ // 其余任何 prompt 都是未预期的,拒绝并使 attempt 失败。
141
+ if (p && typeof p.message === "string" && p.message.includes("Enterprise")) {
142
+ return Promise.resolve("");
143
+ }
144
+ return Promise.reject(new Error("unexpected prompt: " + (p?.message ?? String(p))));
145
+ },
146
+ };
147
+ // 响应先行:begin 以后台任务执行,handler 返回路径不得 await begin。
148
+ void ctx.authorization
149
+ .begin({ key: CREDENTIAL_KEY, method: "oauth", interaction })
150
+ .then((outcome) => {
151
+ // AuthorizationOutcome.status: 'authorized' | 'cancelled'(types.d.ts L68-71)
152
+ if (outcome && outcome.status === "authorized") {
153
+ attempt.status = "authorized";
154
+ void sync();
155
+ } else {
156
+ attempt.status = "failed";
157
+ attempt.error = "登录已取消";
158
+ }
159
+ })
160
+ .catch((err) => {
161
+ attempt.status = "failed";
162
+ attempt.error = String(err?.message ?? err);
163
+ });
164
+ json(res, 202, { ok: true });
165
+ },
166
+ });
167
+
168
+ ctx.webServer.register({
169
+ kind: "exact",
170
+ path: r.state,
171
+ handler: (req, res) => {
172
+ if (!guard(req, res, "GET")) return;
173
+ json(res, 200, attempt);
174
+ },
175
+ });
176
+
177
+ ctx.webServer.register({
178
+ kind: "exact",
179
+ path: r.status,
180
+ handler: async (req, res) => {
181
+ if (!guard(req, res, "GET")) return;
182
+ // describeRecord 只回传 presence,不把含 token 的 GrantRecord 拉进内存。
183
+ let configured = false;
184
+ try {
185
+ const info = await ctx.credentials.describeRecord(CREDENTIAL_KEY);
186
+ configured = info?.configured === true;
187
+ } catch {
188
+ configured = false;
189
+ }
190
+ json(res, 200, { configured, syncError: lastSyncError });
191
+ },
192
+ });
193
+
194
+ ctx.webServer.register({
195
+ kind: "exact",
196
+ path: r.logout,
197
+ handler: async (req, res) => {
198
+ if (!guard(req, res, "POST")) return;
199
+ // 幂等契约:deleteRecord 对不存在记录是 no-op 且正常 resolve,
200
+ // 真实删除与否由随后的 /status 反映。
201
+ await ctx.credentials.deleteRecord(CREDENTIAL_KEY);
202
+ json(res, 200, { ok: true });
203
+ },
204
+ });
205
+ }
206
+
207
+ export default { name, inject, apply };
package/src/shared.mjs CHANGED
@@ -1,11 +1,11 @@
1
- // 两半区共享的 HTTP API 契约。路由前缀固定、不做配置项:
2
- // client 侧 fetch 硬编码同一路径,配置一旦漂移 client 即 404(见计划附录 B)。
3
- export const CREDENTIAL_KEY = "llm-pi-ai/github-copilot";
4
- export const ROUTE_PREFIX = "/copilot-auth";
5
- export const routes = () => ({
6
- start: `${ROUTE_PREFIX}/start`,
7
- state: `${ROUTE_PREFIX}/state`,
8
- status: `${ROUTE_PREFIX}/status`,
9
- logout: `${ROUTE_PREFIX}/logout`,
10
- });
11
- export const emptyState = () => ({ status: "idle", notices: [], error: undefined });
1
+ // 两半区共享的 HTTP API 契约。路由前缀固定、不做配置项:
2
+ // client 侧 fetch 硬编码同一路径,配置一旦漂移 client 即 404(见计划附录 B)。
3
+ export const CREDENTIAL_KEY = "llm-pi-ai/github-copilot";
4
+ export const ROUTE_PREFIX = "/copilot-auth";
5
+ export const routes = () => ({
6
+ start: `${ROUTE_PREFIX}/start`,
7
+ state: `${ROUTE_PREFIX}/state`,
8
+ status: `${ROUTE_PREFIX}/status`,
9
+ logout: `${ROUTE_PREFIX}/logout`,
10
+ });
11
+ export const emptyState = () => ({ status: "idle", notices: [], error: undefined });