@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 +21 -21
- package/README.md +120 -114
- package/cordis.patch.yml +20 -20
- package/package.json +55 -51
- package/src/host.mjs +207 -185
- package/src/shared.mjs +11 -11
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- DSH
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1
|
+
# @inventec/dsh-copilot-auth
|
|
2
|
+
|
|
3
|
+
[](https://github.com/iasiv5/dsh-copilot-auth/releases)
|
|
4
|
+
[](https://www.npmjs.com/package/@inventec/dsh-copilot-auth)
|
|
5
|
+
[](https://github.com/iasiv5/dsh-copilot-auth/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/iasiv5/dsh-copilot-auth/blob/main/LICENSE)
|
|
7
|
+
[](#前置要求)
|
|
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.
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
//
|
|
35
|
-
// (turnkey,用户新增需求 2026-09-04)。
|
|
36
|
-
// 可写集合取交集:凭据 payload.availableModelIds(账号可用)∩ 内置目录已描述。
|
|
37
|
-
// 目录快照外的 id 必须排除——catalog 路由校验要求模型的 wire 协议可解析
|
|
38
|
-
// (api = 路由设置 ?? 目录条目 ?? 路由共享协议),目录外 id 三者皆空会被整体
|
|
39
|
-
// 拒绝;pi-ai 目录更新后它们会自然进入可写集合。
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return true;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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 });
|