@inventec/dsh-copilot-auth 1.2.20 → 1.2.22
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 +8 -9
- package/icon.svg +18 -0
- package/lib/client.js +31 -5
- package/package.json +8 -3
- package/src/copilot-models.mjs +46 -2
- package/src/refresh-flow.mjs +20 -4
- package/src/refresh-service.mjs +83 -38
package/README.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# @inventec/dsh-copilot-auth
|
|
2
2
|
|
|
3
|
-
[](https://github.com/iasiv5/dsh-copilot-auth/releases)
|
|
4
3
|
[](https://www.npmjs.com/package/@inventec/dsh-copilot-auth)
|
|
5
4
|
[](https://github.com/iasiv5/dsh-copilot-auth/actions/workflows/ci.yml)
|
|
6
5
|
[](https://github.com/iasiv5/dsh-copilot-auth/blob/main/LICENSE)
|
|
7
|
-
[](#版本兼容)
|
|
8
7
|
|
|
9
8
|
为 DSH(DeepSeek Harness)内置的 GitHub Copilot 通道补上**网页设备码登录 / 注销**入口,预置开箱即用的 Copilot 提供方路由,并提供**模型列表管理**。用公司 GitHub 账号(带 Copilot 订阅)登录 DSH 的方式与其他 Copilot 客户端一致:**网页 + 设备码**,全程不填 API Token。
|
|
10
9
|
|
|
@@ -54,7 +53,7 @@ dsh plugin --profile web remove @inventec/dsh-copilot-auth # 卸载
|
|
|
54
53
|
| 📦 模型目录兜底 | 首次登录自动写入账号全部可用模型;你已精简 / 定制过的目录**永不覆盖**(重启、重新登录均不重置) |
|
|
55
54
|
| 🎛️ 管理模型列表 | 目标状态编辑:勾选即终态,支持新增、移除、挽留与「对齐账号与目录」(见下节) |
|
|
56
55
|
| 🌐 中 / 英双语 | 跟随 DSH 语言设置自动切换 |
|
|
57
|
-
| 🧪 测试与 CI |
|
|
56
|
+
| 🧪 测试与 CI | 224 条单元测试(ubuntu / windows / macos 三平台矩阵);tag 触发 npm Trusted Publishing 发布(provenance) |
|
|
58
57
|
|
|
59
58
|
## 管理模型列表
|
|
60
59
|
|
|
@@ -64,9 +63,9 @@ pi-ai 的模型目录是打包时硬编码的 JSON:上游新增模型在本机
|
|
|
64
63
|
|
|
65
64
|
- **勾选即终态**:一张分组全表——**可新增**(账号可用 ∩ 目录可解析,默认不勾)、**在列**(默认勾选)、**待删除候选**(在列但账号未报告 / 目录无法解析,默认不勾=删除提案,重新勾选即**挽留**)。应用时被保留模型的对象、顺序与定制逐字保留,被移除模型的定制随模型消失。
|
|
66
65
|
- **快捷动作**:「全选 / 取消全选」作用于全表;「**对齐账号与目录**」仅在漂移时出现,一键把勾选态设为账号 ∩ 可解析;「**同时清除全部模型定制**」复选框仅在存在可清除定制时出现;摘要行常驻显示「应用后共 N 个:保留 · 新增 · 移除」。
|
|
67
|
-
- **证据门控按实际 diff**:纯新增可用 24 小时内同授权的缓存;**移除类变更强制实时账号证据 +
|
|
66
|
+
- **证据门控按实际 diff**:纯新增可用 24 小时内同授权的缓存;**移除类变更强制实时账号证据 + 最新目录并二次确认**(缓存下勾选删除会自动升级为实时预览);空目标需单独确认,落显式空列表且不会被登录自动回填。移除所需的实时证据会在本地会话令牌过期时**自动续期**(仅内存、不落库);续期仍失败时,来源行会给出原因与出路(先用一次模型 / 重新授权 / 稍后重试)。
|
|
68
67
|
- **生效时机**:web / 服务形态先做原子只增补丁、**重启后**生效;desktop(只读 `app.asar`)走进程内注册表注入,**当次生效、无需重启**、不碰官方产物(ADR 0003)。
|
|
69
|
-
- **并发与恢复**:每个操作带服务端签发的 operationId(幂等可查、绝不重复执行);配置并发编辑永远优先(漂移返回 409 要求重新预览);崩溃 / 重启后可恢复,出现冲突时提供「结束旧配置意图」显式闭环。
|
|
68
|
+
- **并发与恢复**:每个操作带服务端签发的 operationId(幂等可查、绝不重复执行);配置并发编辑永远优先(漂移返回 409 要求重新预览);崩溃 / 重启后可恢复,出现冲突时提供「结束旧配置意图」显式闭环。apply 撞上卡死的旧操作(423)且该操作已 `recovery-needed` 时,弹窗与刷新后的页面都会提供同一「结束旧配置意图」出口(know-how 032 缺口闭环)。
|
|
70
69
|
|
|
71
70
|
## 工作原理
|
|
72
71
|
|
|
@@ -80,10 +79,10 @@ pi-ai 的模型目录是打包时硬编码的 JSON:上游新增模型在本机
|
|
|
80
79
|
|
|
81
80
|
| 插件线 | DSH 实测版本 | 说明 |
|
|
82
81
|
|---|---|---|
|
|
83
|
-
| **v1.2.x** | `0.1.7-rc.2
|
|
82
|
+
| **v1.2.x** | `0.1.7-rc.2`、`0.2.0-rc.1/rc.2`、`0.2.1-rc.2/rc.3` | 当前线;`0.2.0` 起注意插件 peer 精确版本守卫;rc.3 Web 冒烟:`/copilot-auth/status` 200、`activated=true`、`settingsNotServed=[]`、`lastError=null` |
|
|
84
83
|
| v1.1.x | `0.1.2-rc.1` ~ `0.1.5-rc.2` | 旧版线;依赖 v1.2.0 已移除的 authorization 挂载补丁,新版 DSH 上无法激活 |
|
|
85
84
|
|
|
86
|
-
逐版本沿革(v1.2.0 ~ v1.2.20 完整说明与已移除能力的语义记录)见 [docs/CHANGELOG.md](./docs/CHANGELOG.md)
|
|
85
|
+
逐版本沿革(v1.2.0 ~ v1.2.20 完整说明与已移除能力的语义记录)见 [docs/CHANGELOG.md](./docs/CHANGELOG.md)。
|
|
87
86
|
|
|
88
87
|
## FAQ
|
|
89
88
|
|
|
@@ -121,7 +120,7 @@ base 模型(GPT-4o / 4.1 一类)不耗 premium requests;premium 模型(C
|
|
|
121
120
|
- 自行 patch `llm-pi-ai` 整段 config 会覆盖预置路由;`settings.yaml` 的 `llm-pi-ai:` 节只能稀疏覆盖字段,无法删除预置路由本身(移除须卸载本插件)
|
|
122
121
|
- 路由前缀 `/copilot-auth` 两侧硬编码,不可配置
|
|
123
122
|
- 模型目录「**只增不更新**」:合并只补充本机从未见过的条目,上游对已有 id 的元数据修正不会同步(ADR 0001)
|
|
124
|
-
- DSH rc 版本耦合:实测 `0.1.7-rc.2` 与 `0.2.
|
|
123
|
+
- DSH rc 版本耦合:实测 `0.1.7-rc.2`、`0.2.0-rc.1/rc.2` 与 `0.2.1-rc.2/rc.3`;rc.3 Web 冒烟见「版本兼容」,peer 仅 `@deepseek-ai/cordis@^4.0.2`
|
|
125
124
|
- DSH 升级后的目录自愈仅在 pi-ai 基线版本不变时按已确认条目只增重放;跨基线且条目缺失时上报 `self-heal-incompatible`,不修改安装树(重新执行一次手动刷新即可在新基线上激活)
|
|
126
125
|
- 并发保护为宿主单进程内 mutex,不支持多实例 / 多进程并发操作同一安装目录
|
|
127
126
|
- 目录 fsync 仅在 POSIX 执行:Windows 无目录 fsync 语义(必然 `EPERM`),直接跳过,一致性由 NTFS 元数据日志保证(v1.2.3 起)
|
|
@@ -131,7 +130,7 @@ base 模型(GPT-4o / 4.1 一类)不耗 premium requests;premium 模型(C
|
|
|
131
130
|
|
|
132
131
|
```bash
|
|
133
132
|
npm install
|
|
134
|
-
npm test # node:test:
|
|
133
|
+
npm test # node:test:224 条(CI 跑 ubuntu/windows/macos 三平台矩阵)
|
|
135
134
|
npm run build # esbuild 打包 client 到 lib/client.js(__ModuleLoader__ 信封)
|
|
136
135
|
npm pack --dry-run
|
|
137
136
|
npm run capture # 截取 README 预览图(需本机 DSH 实例;详见脚本头注)
|
package/icon.svg
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="dsh-copilot-auth">
|
|
2
|
+
<!-- dsh-copilot-auth 认证:斜向钥匙(环尾含青色轴点)= 设备码登录与凭据入库 -->
|
|
3
|
+
<circle cx="12.5" cy="12.5" r="6" stroke="url(#b)" stroke-width="4" fill="none"/>
|
|
4
|
+
<circle cx="12.5" cy="12.5" r="2.2" fill="url(#t)"/>
|
|
5
|
+
<path d="M17 17L27.5 27.5" stroke="url(#b)" stroke-width="4" stroke-linecap="round"/>
|
|
6
|
+
<path d="M24 24L27.5 20.5" stroke="url(#b)" stroke-width="3.4" stroke-linecap="round"/>
|
|
7
|
+
<path d="M27.5 27.5L31 24" stroke="url(#b)" stroke-width="3.4" stroke-linecap="round"/>
|
|
8
|
+
<defs>
|
|
9
|
+
<linearGradient id="b" x1="6.5" y1="6.5" x2="31" y2="27.5" gradientUnits="userSpaceOnUse">
|
|
10
|
+
<stop stop-color="#69B9FF"/>
|
|
11
|
+
<stop offset="1" stop-color="#324DE2"/>
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<linearGradient id="t" x1="10.3" y1="10.3" x2="14.7" y2="14.7" gradientUnits="userSpaceOnUse">
|
|
14
|
+
<stop stop-color="#45E7A4"/>
|
|
15
|
+
<stop offset="1" stop-color="#05909D"/>
|
|
16
|
+
</linearGradient>
|
|
17
|
+
</defs>
|
|
18
|
+
</svg>
|
package/lib/client.js
CHANGED
|
@@ -192,6 +192,9 @@ function reduce(state, event) {
|
|
|
192
192
|
if (event.type === "retire-request" && state.name === "result") {
|
|
193
193
|
return [{ name: "retireConfirm", result: state.result }, null];
|
|
194
194
|
}
|
|
195
|
+
if (event.type === "retire-request" && state.name === "busy" && state.active?.phaseResult?.status === "recovery-needed") {
|
|
196
|
+
return [{ name: "retireConfirm", active: state.active }, null];
|
|
197
|
+
}
|
|
195
198
|
if (event.type === "dismiss" && state.name === "failed") {
|
|
196
199
|
return [{ ...initial, flags: state.flags }, null];
|
|
197
200
|
}
|
|
@@ -200,6 +203,9 @@ function reduce(state, event) {
|
|
|
200
203
|
if (!event.status) return [state, null];
|
|
201
204
|
if (refresh?.lastError === "state-corrupt") return [{ ...state, name: "failed", error: "state-corrupt" }, null];
|
|
202
205
|
const base = fromRefresh({ ...initial, name: "idle" }, refresh);
|
|
206
|
+
if (refresh?.activeOperation?.phaseResult?.status === "recovery-needed") {
|
|
207
|
+
return [{ ...base, name: "busy", active: refresh.activeOperation }, null];
|
|
208
|
+
}
|
|
203
209
|
if (restartStates.has(refresh?.lastResult?.status ?? "") || refresh?.pendingRestart === true) {
|
|
204
210
|
return [{ ...base, name: "pendingRestart", result: refresh.lastResult }, null];
|
|
205
211
|
}
|
|
@@ -341,14 +347,16 @@ function reduce(state, event) {
|
|
|
341
347
|
}
|
|
342
348
|
case "retireConfirm": {
|
|
343
349
|
if (event.type === "retire-confirm") {
|
|
344
|
-
return [{ name: "retiring", operationId: state.result?.operationId, result: state.result }, { type: "retire", operationId: state.result?.operationId }];
|
|
350
|
+
return [{ name: "retiring", operationId: state.result?.operationId ?? state.active?.operationId, result: state.result, active: state.active }, { type: "retire", operationId: state.result?.operationId ?? state.active?.operationId }];
|
|
345
351
|
}
|
|
346
|
-
if (event.type === "cancel") return [{ name: "result", result: state.result }, null];
|
|
352
|
+
if (event.type === "cancel") return state.active ? [{ name: "busy", active: state.active }, null] : [{ name: "result", result: state.result }, null];
|
|
347
353
|
return [state, null];
|
|
348
354
|
}
|
|
349
355
|
case "retiring": {
|
|
350
356
|
if (event.type === "retire-ok") return [{ ...state, name: "checking" }, { type: "status" }];
|
|
351
|
-
if (event.type === "retire-fail")
|
|
357
|
+
if (event.type === "retire-fail") {
|
|
358
|
+
return state.active ? [{ name: "busy", active: state.active, retireError: event.error }, null] : [{ name: "result", result: state.result, retireError: event.error }, null];
|
|
359
|
+
}
|
|
352
360
|
return [state, null];
|
|
353
361
|
}
|
|
354
362
|
default:
|
|
@@ -680,6 +688,10 @@ var DICTS = {
|
|
|
680
688
|
skippedModels: "Upstream entries skipped by validation",
|
|
681
689
|
srcLive: "Account models: live",
|
|
682
690
|
srcCache: "Account models: cached at {time}; removals require upgrading to live evidence",
|
|
691
|
+
srcCacheExpired: "Account models: cached at {time}. The session token is expired and could not be auto-renewed \u2014 chat with any Copilot model once; if it still fails, sign out and authorize again.",
|
|
692
|
+
srcCacheRejected: "Account models: cached at {time}. The local token was rejected by GitHub (possibly re-authorized on another device) \u2014 sign out and authorize again.",
|
|
693
|
+
srcCacheNetwork: "Account models: cached at {time}. Live evidence is temporarily unreachable \u2014 reopen this dialog later to retry.",
|
|
694
|
+
srcCacheNoSession: "Account models: cached at {time}. No local session token \u2014 authorize first.",
|
|
683
695
|
srcStale: "The cache has no trusted timestamp, is expired, or belongs to changed authorization; reference only.",
|
|
684
696
|
srcLatest: "Catalog source: latest pi-ai from npm",
|
|
685
697
|
srcLocal: "Catalog source: local (npm fetch failed)",
|
|
@@ -767,6 +779,10 @@ var DICTS = {
|
|
|
767
779
|
skippedModels: "\u88AB\u6821\u9A8C\u8DF3\u8FC7\u7684\u4E0A\u6E38\u6761\u76EE",
|
|
768
780
|
srcLive: "\u8D26\u53F7\u6A21\u578B\uFF1A\u5B9E\u65F6\u83B7\u53D6",
|
|
769
781
|
srcCache: "\u8D26\u53F7\u6A21\u578B\uFF1A\u7F13\u5B58\uFF0C\u83B7\u53D6\u4E8E {time}\uFF1B\u79FB\u9664\u7C7B\u66F4\u6539\u9700\u5347\u7EA7\u4E3A\u5B9E\u65F6\u8BC1\u636E",
|
|
782
|
+
srcCacheExpired: "\u8D26\u53F7\u6A21\u578B\uFF1A\u7F13\u5B58\uFF0C\u83B7\u53D6\u4E8E {time}\u3002\u4F1A\u8BDD\u4EE4\u724C\u5DF2\u8FC7\u671F\uFF0C\u672A\u80FD\u81EA\u52A8\u7EED\u671F\u2014\u2014\u5148\u7528\u4EFB\u4E00 Copilot \u6A21\u578B\u5BF9\u8BDD\u4E00\u6B21\uFF1B\u82E5\u4ECD\u5931\u8D25\uFF0C\u8BF7\u9000\u51FA\u767B\u5F55\u5E76\u91CD\u65B0\u6388\u6743\u3002",
|
|
783
|
+
srcCacheRejected: "\u8D26\u53F7\u6A21\u578B\uFF1A\u7F13\u5B58\uFF0C\u83B7\u53D6\u4E8E {time}\u3002\u672C\u5730\u4EE4\u724C\u88AB GitHub \u62D2\u7EDD\uFF08\u53EF\u80FD\u5DF2\u5728\u53E6\u4E00\u53F0\u8BBE\u5907\u91CD\u65B0\u6388\u6743\uFF09\u2014\u2014\u8BF7\u9000\u51FA\u767B\u5F55\u5E76\u91CD\u65B0\u6388\u6743\u3002",
|
|
784
|
+
srcCacheNetwork: "\u8D26\u53F7\u6A21\u578B\uFF1A\u7F13\u5B58\uFF0C\u83B7\u53D6\u4E8E {time}\u3002\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\u5B9E\u65F6\u8BC1\u636E\u2014\u2014\u7A0D\u540E\u91CD\u5F00\u6B64\u7A97\u53E3\u91CD\u8BD5\u3002",
|
|
785
|
+
srcCacheNoSession: "\u8D26\u53F7\u6A21\u578B\uFF1A\u7F13\u5B58\uFF0C\u83B7\u53D6\u4E8E {time}\u3002\u672C\u5730\u65E0\u4F1A\u8BDD\u4EE4\u724C\u2014\u2014\u8BF7\u5148\u5B8C\u6210\u6388\u6743\u3002",
|
|
770
786
|
srcStale: "\u7F13\u5B58\u7F3A\u5C11\u53EF\u4FE1\u65F6\u95F4\u3001\u5DF2\u8FC7\u671F\u6216\u6388\u6743\u5DF2\u53D8\u5316\uFF0C\u4EC5\u4F9B\u53C2\u8003\u3002",
|
|
771
787
|
srcLatest: "\u76EE\u5F55\u6765\u6E90\uFF1Anpm \u6700\u65B0 pi-ai",
|
|
772
788
|
srcLocal: "\u76EE\u5F55\u6765\u6E90\uFF1A\u672C\u5730\u76EE\u5F55\uFF08npm \u62C9\u53D6\u5931\u8D25\uFF09",
|
|
@@ -929,6 +945,14 @@ function sampleThemeSurface() {
|
|
|
929
945
|
const bg = m ? `rgb(${m[1].split(",").slice(0, 3).join(",")})` : "#fff";
|
|
930
946
|
return { bg, fg: cs.color || "inherit" };
|
|
931
947
|
}
|
|
948
|
+
function cacheCopyKey(reason) {
|
|
949
|
+
return {
|
|
950
|
+
"access-expired": "srcCacheExpired",
|
|
951
|
+
"token-rejected": "srcCacheRejected",
|
|
952
|
+
"network": "srcCacheNetwork",
|
|
953
|
+
"no-session": "srcCacheNoSession"
|
|
954
|
+
}[reason] ?? null;
|
|
955
|
+
}
|
|
932
956
|
function RefreshModal({ t, flow, onSelect, onConfirm, onCancel, onOverlay }) {
|
|
933
957
|
const p = flow.preview;
|
|
934
958
|
const isStale = p.evidence?.stale === true;
|
|
@@ -984,7 +1008,7 @@ function RefreshModal({ t, flow, onSelect, onConfirm, onCancel, onOverlay }) {
|
|
|
984
1008
|
onConfirm({});
|
|
985
1009
|
};
|
|
986
1010
|
const sourceLine = [
|
|
987
|
-
p.evidence?.source === "live" ? t("srcLive") : p.evidence?.source === "cache" ? t("srcCache").replace("{time}", String(p.evidence?.fetchedAt ?? "")) : null,
|
|
1011
|
+
p.evidence?.source === "live" ? t("srcLive") : p.evidence?.source === "cache" ? (cacheCopyKey(p.evidence?.reason) ? t(cacheCopyKey(p.evidence?.reason)) : t("srcCache")).replace("{time}", String(p.evidence?.fetchedAt ?? "")) : null,
|
|
988
1012
|
p.catalogSource === "latest" ? t("srcLatest") : p.catalogSource === "overlay" ? t("srcOverlay") : t("srcLocal")
|
|
989
1013
|
].filter(Boolean).join(" \xB7 ");
|
|
990
1014
|
const groupDefs = [
|
|
@@ -1319,8 +1343,10 @@ function CopilotSection({ t = (key) => DICTS.en[key] ?? key }) {
|
|
|
1319
1343
|
] }),
|
|
1320
1344
|
flow.name === "busy" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.banner, children: [
|
|
1321
1345
|
"\u24D8 ",
|
|
1322
|
-
t("busy")
|
|
1346
|
+
t("busy"),
|
|
1347
|
+
flow.retireError ? `\uFF08${flow.retireError}\uFF09` : ""
|
|
1323
1348
|
] }),
|
|
1349
|
+
flow.name === "busy" && flow.active?.phaseResult?.status === "recovery-needed" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: { ...styles.button, ...styles.secondary }, onClick: () => drive({ type: "retire-request" }), children: t("retireBtn") }) }),
|
|
1324
1350
|
flow.name === "resultUnknown" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1325
1351
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.error, children: [
|
|
1326
1352
|
"\u26A0 ",
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inventec/dsh-copilot-auth",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "为内置 Copilot 提供方补设备码登录设置页,预置开箱路由;凭据入内置凭据库,模型目录兜底不覆盖定制。v1.2.19:管理模型列表 v3 目标状态编辑(分组全表/挽留/对齐/清空语义告知)+单次确认+移除请求撤回+退出残影修复。v1.2.18:授权失败附网络失败码+人性化提示。适配 DSH 0.1.7+(≤0.1.5 请用 1.1.x)。",
|
|
3
|
+
"version": "1.2.22",
|
|
4
|
+
"description": "为内置 Copilot 提供方补设备码登录设置页,预置开箱路由;凭据入内置凭据库,模型目录兜底不覆盖定制。v1.2.19:管理模型列表 v3 目标状态编辑(分组全表/挽留/对齐/清空语义告知)+单次确认+移除请求撤回+退出残影修复。v1.2.18:授权失败附网络失败码+人性化提示。适配 DSH 0.1.7+(≤0.1.5 请用 1.1.x)。v1.2.21:移除前实时证据自动续期(ephemeral 不落库+负缓存 5min)+缓存证据原因分型文案+busy 卡死 retire 出口与水合判序修正(032)。",
|
|
5
|
+
"icon": "./icon.svg",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"license": "MIT",
|
|
7
8
|
"author": "iasi",
|
|
@@ -38,7 +39,8 @@
|
|
|
38
39
|
"src/model-update.mjs",
|
|
39
40
|
"src/refresh-service.mjs",
|
|
40
41
|
"src/refresh-transaction.mjs",
|
|
41
|
-
"src/catalog-overlay.json"
|
|
42
|
+
"src/catalog-overlay.json",
|
|
43
|
+
"icon.svg"
|
|
42
44
|
],
|
|
43
45
|
"scripts": {
|
|
44
46
|
"build": "node scripts/build-client.mjs",
|
|
@@ -59,6 +61,9 @@
|
|
|
59
61
|
"yaml": "^2.5.0"
|
|
60
62
|
},
|
|
61
63
|
"dsh": {
|
|
64
|
+
"engines": {
|
|
65
|
+
"dsh": ">=0.1.7-rc.2"
|
|
66
|
+
},
|
|
62
67
|
"bundle": {
|
|
63
68
|
"patch": "./cordis.patch.yml"
|
|
64
69
|
},
|
package/src/copilot-models.mjs
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// ⚠️ 显式耦合 pi-ai 0.84.4:COPILOT_HEADERS / COPILOT_API_VERSION / normalizeDomain /
|
|
3
3
|
// getGitHubCopilotBaseUrl / parseGitHubCopilotModelCatalog 的语义逐行抄录自
|
|
4
4
|
// @earendil-works/pi-ai@0.84.4 dist/auth/oauth/github-copilot.js。
|
|
5
|
-
// 不实现 token
|
|
5
|
+
// 不实现 token 获取/policy 修改;token 刷新仅限 v1.2.21 起的 ephemeral 续期(见文末,
|
|
6
|
+
// 只存内存、绝不落库);其余语义漂移或任何失败一律由调用方回退凭证缓存
|
|
6
7
|
// (payload.availableModelIds)。pi-ai 升级后本文件必须对照新版源码复核。
|
|
8
|
+
// v1.2.21 增抄:copilot_internal/v2/token 续期语义抄录自 pi-ai dist/auth/oauth/github-copilot.js
|
|
9
|
+
// refreshGitHubCopilotAccessToken(0.84.4 与 0.87.1 该函数 diff 为空);ephemeral 使用、绝不落库。
|
|
7
10
|
|
|
8
11
|
// ↓↓↓ 以下常量抄录自 pi-ai 0.84.4 dist/auth/oauth/github-copilot.js(勿改值)
|
|
9
12
|
const COPILOT_HEADERS = {
|
|
@@ -107,7 +110,48 @@ export async function fetchLiveAvailableModelIds({ credential, fetchImpl } = {})
|
|
|
107
110
|
signal: AbortSignal.timeout(5000),
|
|
108
111
|
});
|
|
109
112
|
if (!response.ok) {
|
|
110
|
-
|
|
113
|
+
// 携 status 供调用方证据分型(401/403 → token-rejected;其余 → network),与 refreshEphemeralAccess 同款
|
|
114
|
+
throw Object.assign(new Error(`${response.status} ${response.statusText}: GET ${baseUrl}/models`), { status: response.status });
|
|
111
115
|
}
|
|
112
116
|
return parseAvailableModelIds(await response.json(), allowPolicyFallback);
|
|
113
117
|
}
|
|
118
|
+
|
|
119
|
+
// 本地解析 access(tid 会话令牌)内的 exp=(秒 → 毫秒);缺失/不可解析 → null。
|
|
120
|
+
// 零网络:调用方据此在发请求前判断"是否需要续期"。
|
|
121
|
+
export function parseAccessExpiryMs(access) {
|
|
122
|
+
const match = String(access ?? "").match(/exp=(\d+)/);
|
|
123
|
+
return match ? Number(match[1]) * 1000 : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 铸一枚新的短命 tid:GET copilot_internal/v2/token(Bearer = 长期 ghu)。
|
|
127
|
+
// 语义抄录自 pi-ai refreshGitHubCopilotAccessToken(见文件头);单次尝试、5s 超时、
|
|
128
|
+
// 不重试、结果仅存内存(调用方绝不写回凭据库)。HTTP 失败抛错并携 status 供分型
|
|
129
|
+
// (401/403 → 长期令牌被拒;其余/网络 → network)。错误消息保真对齐 0.87.1 L261/L264。
|
|
130
|
+
export async function refreshEphemeralAccess({ refreshToken, enterpriseUrl, fetchImpl } = {}) {
|
|
131
|
+
if (typeof refreshToken !== "string" || refreshToken === "" || typeof fetchImpl !== "function") {
|
|
132
|
+
throw new Error("refreshToken and fetchImpl required");
|
|
133
|
+
}
|
|
134
|
+
const domain = normalizeDomain(enterpriseUrl) ?? "github.com";
|
|
135
|
+
const url = `https://api.${domain}/copilot_internal/v2/token`;
|
|
136
|
+
const response = await fetchImpl(url, {
|
|
137
|
+
headers: {
|
|
138
|
+
Accept: "application/json",
|
|
139
|
+
Authorization: `Bearer ${refreshToken}`,
|
|
140
|
+
...COPILOT_HEADERS,
|
|
141
|
+
},
|
|
142
|
+
signal: AbortSignal.timeout(5000),
|
|
143
|
+
});
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw Object.assign(new Error(`${response.status} ${response.statusText}: GET ${url}`), { status: response.status });
|
|
146
|
+
}
|
|
147
|
+
const raw = await response.json();
|
|
148
|
+
if (!raw || typeof raw !== "object") {
|
|
149
|
+
throw new Error("Invalid Copilot token response");
|
|
150
|
+
}
|
|
151
|
+
const token = raw.token;
|
|
152
|
+
const expiresAt = raw.expires_at;
|
|
153
|
+
if (typeof token !== "string" || typeof expiresAt !== "number") {
|
|
154
|
+
throw new Error("Invalid Copilot token response fields");
|
|
155
|
+
}
|
|
156
|
+
return { access: token, expiresAtMs: expiresAt * 1000 - 5 * 60 * 1000 };
|
|
157
|
+
}
|
package/src/refresh-flow.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// 状态:idle → previewing → confirming(勾选→materialize 循环;缓存证据+删除提案时
|
|
4
4
|
// 自动 live 升级:闩锁、硬失败回退 materialize、软降级置 staleNotice)→ applying →
|
|
5
5
|
// result / pendingRestart / checking / busy / resultUnknown;recovery-needed 经
|
|
6
|
-
// retireConfirm → retiring →
|
|
6
|
+
// retireConfirm → retiring → 重新预览闭环;busy 态在 active.phaseResult.status ===
|
|
7
|
+
// "recovery-needed" 时同样提供 retire 出口(032),水合时该判据优先于旧 lastResult
|
|
8
|
+
// (防旧 "applied" 遮蔽卡死操作),retire 失败回 busy 可重试。
|
|
7
9
|
// 关键规则:
|
|
8
10
|
// - 勾选集合即终态;选择变化经 basePreviewId 从原快照 materialize(不重新取数)
|
|
9
11
|
// - 确认门按实际 diff:removed>0 ∨ clearOverrides ∨ 空目标 → 二次确认;空目标另需 empty
|
|
@@ -108,6 +110,10 @@ export function reduce(state, event) {
|
|
|
108
110
|
if (event.type === "retire-request" && state.name === "result") {
|
|
109
111
|
return [{ name: "retireConfirm", result: state.result }, null];
|
|
110
112
|
}
|
|
113
|
+
// 032:busy 态的 retire 出口——仅当卡死操作已 recovery-needed(静止核实后清,绝不写配置)
|
|
114
|
+
if (event.type === "retire-request" && state.name === "busy" && state.active?.phaseResult?.status === "recovery-needed") {
|
|
115
|
+
return [{ name: "retireConfirm", active: state.active }, null];
|
|
116
|
+
}
|
|
111
117
|
if (event.type === "dismiss" && state.name === "failed") {
|
|
112
118
|
return [{ ...initial, flags: state.flags }, null];
|
|
113
119
|
}
|
|
@@ -116,6 +122,10 @@ export function reduce(state, event) {
|
|
|
116
122
|
if (!event.status) return [state, null];
|
|
117
123
|
if (refresh?.lastError === "state-corrupt") return [{ ...state, name: "failed", error: "state-corrupt" }, null];
|
|
118
124
|
const base = fromRefresh({ ...initial, name: "idle" }, refresh);
|
|
125
|
+
// 032 水合判序:卡死操作(recovery-needed)优先于旧 lastResult,防旧 "applied" 遮蔽 busy 死锁
|
|
126
|
+
if (refresh?.activeOperation?.phaseResult?.status === "recovery-needed") {
|
|
127
|
+
return [{ ...base, name: "busy", active: refresh.activeOperation }, null];
|
|
128
|
+
}
|
|
119
129
|
if (restartStates.has(refresh?.lastResult?.status ?? "") || refresh?.pendingRestart === true) {
|
|
120
130
|
return [{ ...base, name: "pendingRestart", result: refresh.lastResult }, null];
|
|
121
131
|
}
|
|
@@ -271,14 +281,20 @@ export function reduce(state, event) {
|
|
|
271
281
|
}
|
|
272
282
|
case "retireConfirm": {
|
|
273
283
|
if (event.type === "retire-confirm") {
|
|
274
|
-
|
|
284
|
+
// 双入口:result 入口携 result;busy 入口(032)携 active——operationId 二选一
|
|
285
|
+
return [{ name: "retiring", operationId: state.result?.operationId ?? state.active?.operationId, result: state.result, active: state.active }, { type: "retire", operationId: state.result?.operationId ?? state.active?.operationId }];
|
|
275
286
|
}
|
|
276
|
-
if (event.type === "cancel") return [{ name: "result", result: state.result }, null];
|
|
287
|
+
if (event.type === "cancel") return state.active ? [{ name: "busy", active: state.active }, null] : [{ name: "result", result: state.result }, null];
|
|
277
288
|
return [state, null];
|
|
278
289
|
}
|
|
279
290
|
case "retiring": {
|
|
280
291
|
if (event.type === "retire-ok") return [{ ...state, name: "checking" }, { type: "status" }];
|
|
281
|
-
if (event.type === "retire-fail")
|
|
292
|
+
if (event.type === "retire-fail") {
|
|
293
|
+
// 032:busy 入口失败回 busy 可重试;result 入口维持既有落点
|
|
294
|
+
return state.active
|
|
295
|
+
? [{ name: "busy", active: state.active, retireError: event.error }, null]
|
|
296
|
+
: [{ name: "result", result: state.result, retireError: event.error }, null];
|
|
297
|
+
}
|
|
282
298
|
return [state, null];
|
|
283
299
|
}
|
|
284
300
|
default:
|
package/src/refresh-service.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// refresh-service.mjs — 预览服务与账号证据(T8)。
|
|
2
2
|
// 职责:内存预览快照(10 分钟、选择从原快照重算、operationId 绑定选择)、
|
|
3
|
-
// 账号证据门禁(移除类变更需 live+latest;纯新增允许 ≤24h 同 intentVersion 缓存;stale
|
|
3
|
+
// 账号证据门禁(移除类变更需 live+latest;纯新增允许 ≤24h 同 intentVersion 缓存;stale 只参考不可应用)
|
|
4
|
+
// +tid 过期自愈(v2/token 铸 ephemeral tid:内存、不落库、单次尝试、负缓存 5min、按 exp 复用)、
|
|
4
5
|
// apply 幂等(同 operationId 返回已知结果;activeOperation 未终结时新 apply 423)、
|
|
5
6
|
// boot/apply/retire 同一队列串行。
|
|
6
7
|
// 事务内核(T9)经 transaction 注入:{runApply({snapshot}), bootRecover(), retire({operationId})}。
|
|
@@ -11,10 +12,11 @@ import { join } from "node:path";
|
|
|
11
12
|
import { readJson, writeJsonAtomic, createMutex } from "./atomic-json.mjs";
|
|
12
13
|
import { CREDENTIAL_KEY } from "./shared.mjs";
|
|
13
14
|
import { buildModelChange } from "./model-update.mjs";
|
|
14
|
-
import { fetchLiveAvailableModelIds } from "./copilot-models.mjs";
|
|
15
|
+
import { fetchLiveAvailableModelIds, parseAccessExpiryMs, refreshEphemeralAccess } from "./copilot-models.mjs";
|
|
15
16
|
|
|
16
17
|
const PREVIEW_TTL_MS = 10 * 60 * 1000;
|
|
17
18
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
const HEAL_RETRY_MS = 5 * 60 * 1000; // 自愈失败负缓存:期间不再打 v2/token
|
|
18
20
|
|
|
19
21
|
function flowError(code, message, extra = {}) {
|
|
20
22
|
const err = new Error(message ?? code);
|
|
@@ -25,40 +27,6 @@ function flowError(code, message, extra = {}) {
|
|
|
25
27
|
|
|
26
28
|
const iso = (ts) => new Date(ts).toISOString();
|
|
27
29
|
|
|
28
|
-
// 账号证据:live GET 成功才写缓存(fetchedAt+intentVersion);缓存仅 ≤24h 且同
|
|
29
|
-
// intentVersion 时可作纯新增的应用依据;否则 stale——预览可展示参考,apply 被拒(Q7/Q16)。
|
|
30
|
-
// 移除类变更(removed>0)在 apply 时强制 live 证据+latest 目录(removals-need-live,Q35/R2);
|
|
31
|
-
// live 拉取失败回退有效缓存属软降级,由 client 端升级/禁用逻辑兜底(R4-3),服务端门最终兜底。
|
|
32
|
-
async function resolveAccountEvidence(ctx, { scope, readIntentVersion, clock, fetchImpl }) {
|
|
33
|
-
const iv = readIntentVersion();
|
|
34
|
-
const cacheFile = join(scope.dataDir, "account-model-cache.json");
|
|
35
|
-
const readCache = () => {
|
|
36
|
-
try {
|
|
37
|
-
return readJson(cacheFile);
|
|
38
|
-
} catch {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
const record = await ctx.credentials.readRecord(CREDENTIAL_KEY);
|
|
43
|
-
const payload = record?.payload ?? {};
|
|
44
|
-
if (payload.access) {
|
|
45
|
-
try {
|
|
46
|
-
const ids = await fetchLiveAvailableModelIds({ credential: payload, fetchImpl });
|
|
47
|
-
const fetchedAt = iso(clock.now()); // 单次取时:缓存值与返回值同源(避免毫秒漂移)
|
|
48
|
-
mkdirSync(scope.dataDir, { recursive: true });
|
|
49
|
-
writeJsonAtomic(cacheFile, { ids, fetchedAt, intentVersion: iv });
|
|
50
|
-
return { source: "live", ids, fetchedAt, stale: false };
|
|
51
|
-
} catch { /* 回退缓存判定 */ }
|
|
52
|
-
}
|
|
53
|
-
const cache = readCache();
|
|
54
|
-
if (cache && Array.isArray(cache.ids) && typeof cache.fetchedAt === "string"
|
|
55
|
-
&& Number.isFinite(Date.parse(cache.fetchedAt))
|
|
56
|
-
&& clock.now() - Date.parse(cache.fetchedAt) <= CACHE_TTL_MS
|
|
57
|
-
&& cache.intentVersion === iv) {
|
|
58
|
-
return { source: "cache", ids: cache.ids, fetchedAt: cache.fetchedAt, stale: false };
|
|
59
|
-
}
|
|
60
|
-
return { source: "stale", ids: Array.isArray(cache?.ids) ? cache.ids : [], fetchedAt: cache?.fetchedAt ?? null, stale: true };
|
|
61
|
-
}
|
|
62
30
|
|
|
63
31
|
export function createRefreshService(ctx, {
|
|
64
32
|
scope,
|
|
@@ -74,6 +42,80 @@ export function createRefreshService(ctx, {
|
|
|
74
42
|
};
|
|
75
43
|
const queue = createMutex();
|
|
76
44
|
const snapshots = new Map(); // previewId -> snapshot(内存态;重启即失效,幂等走持久状态)
|
|
45
|
+
// 自愈三态(N1,纯内存;dispose/重启即清):已铸的 ephemeral tid、失败负缓存时刻与原因
|
|
46
|
+
let ephemeral = null;
|
|
47
|
+
let healBlockedUntil = 0;
|
|
48
|
+
let healBlockedReason = null;
|
|
49
|
+
|
|
50
|
+
// 账号证据(v1.2.21 起闭包内化):live GET 成功才写缓存(fetchedAt+intentVersion);缓存仅 ≤24h
|
|
51
|
+
// 且同 intentVersion 时可作纯新增的应用依据;否则 stale——预览可展示参考,apply 被拒(Q7/Q16)。
|
|
52
|
+
// 移除类变更(removed>0)在 apply 时强制 live 证据+latest 目录(removals-need-live,Q35/R2)。
|
|
53
|
+
// 自愈(N1):tid 过期且记录带 refresh 时经 v2/token 铸 ephemeral tid——a 复用(未过 exp 不重铸)、
|
|
54
|
+
// b 负缓存期内沿用上次失败原因、c 单次续期;任何失败沿用软降级回退有效缓存,并给出 reason 分型。
|
|
55
|
+
async function resolveAccountEvidence({ clock, fetchImpl }) {
|
|
56
|
+
const iv = readIntentVersion();
|
|
57
|
+
const cacheFile = join(scope.dataDir, "account-model-cache.json");
|
|
58
|
+
const readCache = () => {
|
|
59
|
+
try {
|
|
60
|
+
return readJson(cacheFile);
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const record = await ctx.credentials.readRecord(CREDENTIAL_KEY);
|
|
66
|
+
const payload = record?.payload ?? {};
|
|
67
|
+
let reason = null;
|
|
68
|
+
let access = payload?.access;
|
|
69
|
+
if (!access) {
|
|
70
|
+
reason = "no-session";
|
|
71
|
+
} else {
|
|
72
|
+
const expMs = parseAccessExpiryMs(access);
|
|
73
|
+
const accessExpired = expMs !== null && expMs <= clock.now();
|
|
74
|
+
if (accessExpired && typeof payload.refresh === "string" && payload.refresh !== "") {
|
|
75
|
+
if (ephemeral && clock.now() < ephemeral.expiresAtMs) {
|
|
76
|
+
access = ephemeral.access; // a. 复用:不打续期
|
|
77
|
+
} else if (clock.now() < healBlockedUntil) {
|
|
78
|
+
reason = healBlockedReason ?? "access-expired"; // b. 负缓存:沿用上次失败原因
|
|
79
|
+
access = undefined;
|
|
80
|
+
} else {
|
|
81
|
+
try { // c. 续期:单次尝试,成功清负缓存
|
|
82
|
+
const minted = await refreshEphemeralAccess({ refreshToken: payload.refresh, enterpriseUrl: payload.enterpriseUrl, fetchImpl });
|
|
83
|
+
ephemeral = minted;
|
|
84
|
+
healBlockedUntil = 0;
|
|
85
|
+
healBlockedReason = null;
|
|
86
|
+
if (clock.now() < minted.expiresAtMs) access = minted.access;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
healBlockedUntil = clock.now() + HEAL_RETRY_MS;
|
|
89
|
+
healBlockedReason = error?.status === 401 || error?.status === 403 ? "token-rejected" : "network";
|
|
90
|
+
reason = healBlockedReason;
|
|
91
|
+
access = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} else if (accessExpired) {
|
|
95
|
+
reason = "access-expired"; // 过期且无 refresh:无从续期,跳过 live GET
|
|
96
|
+
access = undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (access) {
|
|
100
|
+
try {
|
|
101
|
+
const ids = await fetchLiveAvailableModelIds({ credential: { ...payload, access }, fetchImpl });
|
|
102
|
+
const fetchedAt = iso(clock.now()); // 单次取时:缓存值与返回值同源(避免毫秒漂移)
|
|
103
|
+
mkdirSync(scope.dataDir, { recursive: true });
|
|
104
|
+
writeJsonAtomic(cacheFile, { ids, fetchedAt, intentVersion: iv });
|
|
105
|
+
return { source: "live", ids, fetchedAt, stale: false, reason: null };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
reason = error?.status === 401 || error?.status === 403 ? "token-rejected" : "network";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const cache = readCache();
|
|
111
|
+
if (cache && Array.isArray(cache.ids) && typeof cache.fetchedAt === "string"
|
|
112
|
+
&& Number.isFinite(Date.parse(cache.fetchedAt))
|
|
113
|
+
&& clock.now() - Date.parse(cache.fetchedAt) <= CACHE_TTL_MS
|
|
114
|
+
&& cache.intentVersion === iv) {
|
|
115
|
+
return { source: "cache", ids: cache.ids, fetchedAt: cache.fetchedAt, stale: false, reason };
|
|
116
|
+
}
|
|
117
|
+
return { source: "stale", ids: Array.isArray(cache?.ids) ? cache.ids : [], fetchedAt: cache?.fetchedAt ?? null, stale: true, reason };
|
|
118
|
+
}
|
|
77
119
|
const getFetch = () => fetchImpl ?? globalThis.fetch?.bind(globalThis);
|
|
78
120
|
const resolveCatalog = deps.resolveCatalog;
|
|
79
121
|
const describeConfigView = deps.describeConfigView;
|
|
@@ -140,7 +182,7 @@ export function createRefreshService(ctx, {
|
|
|
140
182
|
snapshots.set(snap.previewId, snap);
|
|
141
183
|
return projectPreview(snap);
|
|
142
184
|
}
|
|
143
|
-
const evidence = await resolveAccountEvidence(
|
|
185
|
+
const evidence = await resolveAccountEvidence({ clock, fetchImpl: getFetch() });
|
|
144
186
|
const catalog = await resolveCatalog(catalogSource);
|
|
145
187
|
const config = await describeConfigView();
|
|
146
188
|
const intentVersion = readIntentVersion();
|
|
@@ -174,7 +216,7 @@ export function createRefreshService(ctx, {
|
|
|
174
216
|
operationId: snap.operationId,
|
|
175
217
|
operation: snap.operation,
|
|
176
218
|
expiresAt: iso(snap.expiresAt),
|
|
177
|
-
evidence: { source: snap.evidence.source, fetchedAt: snap.evidence.fetchedAt, stale: snap.evidence.stale },
|
|
219
|
+
evidence: { source: snap.evidence.source, fetchedAt: snap.evidence.fetchedAt, stale: snap.evidence.stale, reason: snap.evidence.reason ?? null },
|
|
178
220
|
catalogSource: snap.catalog.catalogSource,
|
|
179
221
|
catalogError: snap.catalog.catalogError ?? null,
|
|
180
222
|
skipped: snap.catalog.skipped ?? [],
|
|
@@ -280,6 +322,9 @@ export function createRefreshService(ctx, {
|
|
|
280
322
|
|
|
281
323
|
function dispose() {
|
|
282
324
|
snapshots.clear();
|
|
325
|
+
ephemeral = null;
|
|
326
|
+
healBlockedUntil = 0;
|
|
327
|
+
healBlockedReason = null;
|
|
283
328
|
}
|
|
284
329
|
|
|
285
330
|
// 诊断/测试观察面:当前内存快照数(含未过期;过期项在下次 preview 入口清扫)
|